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.
api://{web api client id}/.default
. Also, you can check the token by using jwt.io. – Tony JuclientApplication.acquireTokenSilent(scope)
– Tony Juapi://client_id
. – Tony Ju