0
votes

I'm trying to create a simple .Net Core Web Api in Azure to test authentication using JQuery. I managed to resolve the CORS issue but I keep getting a 401 "the issuer is invalid" error when trying to use the bearer token. I was able to test the Web Api using Postman and a secret but not when using JQuery and AAD. I lifted some demo SPA code from a sample that works separately but not when I keep the client and Web Api in separate projects. I thought maybe it was required for me to use the client ID of the Web Api to get my token but that doesn't seem to have any effect. The controller couldn't be any more basic.

namespace test_core_web_api_spa.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
            // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
            // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803
        }
    }
}

And the startup for the Web Api is basic.

namespace test_core_web_api_spa
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials());
            });
            services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
                .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseAuthentication();
            app.UseCors(options => options.WithOrigins("https://localhost:44399", "https://localhost:44308").AllowAnyMethod().AllowAnyHeader());
            app.UseMvc();
        }
    }
}

The HTML page was copied from a SPA demo using AAD.

<!DOCTYPE html>
<html>
<head>
    <title>Test API Call</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
    <link rel="stylesheet" href="/css/app.css">
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed"
                        data-toggle="collapse"
                        data-target=".navbar-collapse">

                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/#Home">test api call</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a href="/#Home">Home</a></li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    <li class="app-user navbar-text"></li>
                    <li><a href="javascript:;" class="app-logout">Logout</a></li>
                    <li><a href="javascript:;" class="app-login">Login</a></li>
                </ul>
            </div>
        </div>
    </div>

    <div id="divHome" class="container-fluid">
        <div class="jumbotron">
            <h5 id="WelcomeMessage"></h5>
            <div class="text-hide">Surname: <span id="userSurName"></span><span id="userEmail"></span></div>
            <h2>test page</h2>
        </div>

        <div>
            <br />
            <a href="javascript:;" class="btnCallApiTest">Call Api</a>
            <br />
            <p class="view-loading">Loading...</p>
            <div class="app-error"></div>
            <br />
            <span id="data-container"></span>
        </div>
        <br />
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.3.4/bluebird.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.1.3/js/msal.js" integrity="sha384-m/3NDUcz4krpIIiHgpeO0O8uxSghb+lfBTngquAo2Zuy2fEF+YgFeP08PWFo5FiJ" crossorigin="anonymous"></script>
    <script src="js/rest_api.js"></script>

</body>
</html>

And the Javascript was also borrowed from the SPA AAD demo in GitHub.

// the AAD application
var clientApplication;

(function () {
    console.log("document ready done");
    window.config = {
        clientID: 'clientidof_web_api_in_azure'
    };

    const loginRequest = {
        scopes: ["openid", "profile", "User.Read"]
    };
    const tokenRequest2 = {
        scopes: ["https://myPortal.onmicrosoft.com/test_core_web_api_spa"]
    };
    var scope = [window.config.clientID];
    const msalConfigDemo = {
            auth: {
                clientId: "myclientid", 
                authority: "https://login.microsoftonline.com/mytenantid",
                consentScopes: ["user.read","https://myportal.onmicrosoft.com/test_core_web_api_spa/user_impersonation"],
                validateAuthority: true
            },
            cache: {
                cacheLocation: "localStorage",
                storeAuthStateInCookie: false
            }
    };
    function authCallback(errorDesc, token, error, tokenType) {
        //This function is called after loginRedirect and acquireTokenRedirect. Not called with loginPopup
        // msal object is bound to the window object after the constructor is called.
        if (token) {
            log("authCallback success");
            console.log({ 'token': token });
            console.log({ 'tokenType': tokenType });
        }
        else {
            log(error + ":" + errorDesc);
        }
    }

    if (!clientApplication) {
        clientApplication = new clientApplication = new Msal.UserAgentApplication(msalConfigDemo, msalConfigDemo, authCallback);
    } else {
        console.log({ 'clientApplication': clientApplication });
    }

    // Get UI jQuery Objects
    var $panel = $(".panel-body");
    var $userDisplay = $(".app-user");
    var $signInButton = $(".app-login");
    var $signOutButton = $(".app-logout");
    var $errorMessage = $(".app-error");
    var $btnCallApiTest = $(".btnCallApiTest");
    onSignin(null);


    // Handle Navigation Directly to View
    window.onhashchange = function () {
        loadView(stripHash(window.location.hash));
    };
    window.onload = function () {
        $(window).trigger("hashchange");
    };

    $btnCallApiTest.click(function () {
        call_api_test();
    });

    // Register NavBar Click Handlers
    $signOutButton.click(function () {
        clientApplication.logout();
    });

    $signInButton.click(function () {
        clientApplication.loginPopup(loginRequest).then(onSignin);
    });


    function stripHash(view) {
        return view.substr(view.indexOf('#') + 1);
    }

    function call_api_test() {
        // Empty Old View Contents
        var $dataContainer = $(".data-container");
        $dataContainer.empty();
        var $loading = $(".view-loading");

        clientApplication.acquireTokenSilent(tokenRequest2)
            .then(function (token) {
                getTodoList(token.accessToken, $dataContainer, $loading);
            }, function (error) {
                clientApplication.acquireTokenPopup(tokenRequest2).then(function (token) {
                    getTodoList(token.accessToken, $dataContainer, $loading);
                }, function (error) {
                    printErrorMessage(error);
                });
            });
    }

    function getTodoList(accessToken, dataContainer, loading) {
        // Get TodoList Data
        let urlstring = 'https://localhost:44363/api/values';
        console.log({ 'accessToken': accessToken });
        $.ajax({
            type: "GET",
            url: urlstring,
            headers: {
                'Authorization': 'Bearer ' + accessToken,
            },
        }).done(function (data) {
            // Update the UI
            console.log({ 'data': data });
            loading.hide();
            dataContainer.html(data);
        }).fail(function (jqXHR, textStatus) {
            printErrorMessage('Error getting todo list data statusText->' + textStatus + ' status->' + jqXHR.status);
            console.log({ 'jqXHR': jqXHR });
            loading.hide();
        }).always(function () {
            // Register Handlers for Buttons in Data Table
            //registerDataClickHandlers();
        });
    }

    function printErrorMessage(mes) {
        var $errorMessage = $(".app-error");
        $errorMessage.html(mes);
    }
    function onSignin(idToken) {
        // Check Login Status, Update UI
        var user = clientApplication.getUser();
        if (user) {
            $userDisplay.html(user.name);
            $userDisplay.show();
            $signInButton.hide();
            $signOutButton.show();
        } else {
            $userDisplay.empty();
            $userDisplay.hide();
            $signInButton.show();
            $signOutButton.hide();
        }

    }
}());

Logging in does work with AAD and pulls back my email address. I do see a token created and being passed to the Web Api. But then it gives the "issuer is invalid" 401 error. I'm the client Id of the Web Api when making my token request so I'm not sure what else could be changed.

Per comments I have tried to pass a scope to the loginPopup call.

    $signInButton.click(function () {
        clientApplication.loginPopup(requestObj).then(onSignin);
    });

However the only value that works gives the same results:

var requestObj = ["web-api-client-id"];

I've tried the URL of the local web service running including combinations using https://localhost:44399/.default but that throws immediate errors before getting a token with a message like the resource principal named https://localhost:44399 was not found in the tenant. If the problem is the scope setting in this call then I am not sure what value to use to make this work locally when debugging. As a side note I found other Github samples using a format of

var requestObj = {scopes: ["api://clientid/access_as_user"]};

but these fail to execute saying API does not accept non-array scopes. I might ask this in a separate thread.

Update Nov 13 I switched from 0.2.3 of MSAL to 1.1.3 and then updated the logic to reflect the changes made in the different versions.

I also confirmed the client app has API permissions to the web api. I added a new scope to the web api called "user_impersonation". The existing "api-access" was locked to admin control in my tenant.

When trying to use the form "api//" it does not find the resource. Here are the values I tried which all get the same error. I think this format is legacy.


scopes: ["api://web-api-clientid"]
scopes: ["api://web-api-clientid/api-access"]
scopes: ["api://web-api-clientid/user_impersonation"]
scopes: ["api://web-api-clientid/.default"]
ServerError: AADSTS500011: The resource principal named api://web-api-clientid was not found in the tenant named my-tenant-id. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.

When trying these scopes the error was 401 audience is invalid.

scopes: ["https://myPortal.onmicrosoft.com/test_core_web_api_spa/user_impersonation"]
scopes: ["https://myPortal.onmicrosoft.com/test_core_web_api_spa/.default"]
401 www-authenticate: Bearer error="invalid_token", error_description="The audience is invalid" 
"aud": "https://myPortal.onmicrosoft.com/test_core_web_api_spa"

When trying these scopes the error message gets my client app correct but again doesn't seem to think my web api app exists on my portal.

scopes: ["https://myPortal.onmicrosoft.com/test_core_web_api_spa"]
scopes: ["https://myPortal.onmicrosoft.com/test_core_web_api_spa","user_impersonation"]
ServerError: AADSTS650053: The application 'demoapp-frontend' asked for scope 'test_core_web_api_spa' that doesn't exist on the resource 'myportal_guid'. 

Sorry for all the confusion but I've been trying everything and the code is getting messy. I'm almost to the point where I might need to start over again.

2
The scope is not correct. You should use something like api://{web api client id}/.default. Also, you can check the token by using jwt.io.Tony Ju
The scope was based on the sample provided at github.com/Azure-Samples/… which shows that the scope should be the clientID as registered in Azure. The file specifically was github.com/Azure-Samples/… .pretzelb
Try to change the scope here clientApplication.acquireTokenSilent(scope)Tony Ju
You can check the token by decoding it. The value of aud should be api://client_id.Tony Ju
You should not use the client id only as scope, as that will get you an id token. What you need is an access token.juunas

2 Answers

0
votes

Modify your code as below

window.config = {
        clientID: 'clientidof_web_client'
    };
    var scope = ["api://{clientidof_web_api}/.default"];

You can check the token by decoding it. The value of aud should be api://client_id_of_web_api

enter image description here

Update:

Have you added your api to your client app permission?

enter image description here

0
votes

The problem was the configuration data for the Web API. When they say the ClientId what they really want is the value under the "expose an API" option where it says "Application ID URI". What I was putting in there was the guid for the Web Api application registration. Below is how it should look.

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "myportal.onmicrosoft.com",
    "TenantId": "mytenant-guid",
    "ClientId": "https://myportal.onmicrosoft.com/test_core_web_api_spa"
  },