【发布时间】:2020-03-04 13:46:26
【问题描述】:
我正在尝试在 Azure 中创建一个简单的 .Net Core Web Api 来测试使用 JQuery 的身份验证。我设法解决了 CORS 问题,但在尝试使用不记名令牌时,我不断收到 401“发行者无效”错误。我能够使用 Postman 和秘密测试 Web Api,但在使用 JQuery 和 AAD 时却不行。我从一个单独工作的示例中提取了一些演示 SPA 代码,但当我将客户端和 Web Api 保存在单独的项目中时,我没有。我想我可能需要使用 Web Api 的客户端 ID 来获取我的令牌,但这似乎没有任何效果。控制器再简单不过了。
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
}
}
}
Web Api 的启动是基本的。
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();
}
}
}
HTML 页面是从使用 AAD 的 SPA 演示中复制而来的。
<!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>
而且 Javascript 也是从 GitHub 的 SPA AAD 演示中借来的。
// 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();
}
}
}());
登录确实可以使用 AAD 并拉回我的电子邮件地址。我确实看到创建了一个令牌并将其传递给 Web Api。但随后它给出了“发行者无效”401错误。在发出令牌请求时,我是 Web Api 的客户端 ID,所以我不确定还有什么可以更改的。
我尝试将范围传递给loginPopup 调用。
$signInButton.click(function () {
clientApplication.loginPopup(requestObj).then(onSignin);
});
但是,唯一有效的值给出了相同的结果:
var requestObj = ["web-api-client-id"];
我已尝试运行本地 Web 服务的 URL,包括使用 https://localhost:44399/.default 的组合,但在获取带有类似 the resource principal named https://localhost:44399 was not found in the tenant 消息的令牌之前会立即引发错误。如果问题是此调用中的范围设置,那么我不确定在调试时使用什么值来使其在本地工作。作为旁注,我发现其他 Github 示例使用的格式为
var requestObj = {scopes: ["api://clientid/access_as_user"]};
但这些无法执行说API does not accept non-array scopes。我可能会在单独的线程中问这个问题。
11 月 13 日更新 我从 MSAL 的 0.2.3 切换到 1.1.3,然后更新了逻辑以反映不同版本中所做的更改。
我还确认客户端应用程序对 Web api 具有 API 权限。我向 Web api 添加了一个名为“user_impersonation”的新范围。现有的“api-access”在我的租户中被锁定为管理员控制。
当尝试使用“api//”形式时,它找不到资源。这是我尝试过的值,它们都得到相同的错误。我认为这种格式是遗留的。
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.
在尝试这些范围时,错误是 401 受众无效。
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"
在尝试这些范围时,错误消息使我的客户端应用程序正确,但似乎我的门户网站上不存在我的 Web api 应用程序。
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'.
对于所有的混乱,我很抱歉,但我一直在尝试一切,代码变得混乱。我几乎到了可能需要重新开始的地步。
【问题讨论】:
-
范围不正确。你应该使用类似
api://{web api client id}/.default的东西。此外,您可以使用jwt.io 来检查令牌。 -
范围基于github.com/Azure-Samples/… 提供的示例,这表明范围应该是在 Azure 中注册的 clientID。该文件具体为github.com/Azure-Samples/…。
-
尝试在此处更改范围
clientApplication.acquireTokenSilent(scope) -
您可以通过解码来检查令牌。 aud 的值应该是
api://client_id。 -
您不应仅将客户端 ID 用作范围,因为这将为您提供 id 令牌。你需要的是一个访问令牌。
标签: javascript jquery authentication asp.net-web-api azure-active-directory