JWT 不记名令牌:"The audience 'api://...' is invalid"
JWT Bearer Token: "The audience 'api://...' is invalid"
我正在研究 Les Jackson 的 The Complete ASP.NET Core 3 API Tutorial,但我被卡住了。我在第 14 章“保护我们的 API”。
在本章中,我们应该做的是配置我们构建的 API 以接受来自 Azure Active Directory 的 JWT 令牌,然后构建一个获取 JWT 令牌的客户端来自 Azure Active Directory,然后将其作为自定义 header 包含在针对 API.
的调用中
此时,我们仍然是 运行 本地主机上的 API,接下来是在 Azure 上托管它。
在所有这些中,我使用的是 Azure 中的默认目录。
将 Azure AD JWT 添加到 API:
本章列出了以下步骤:
- 在 Azure AD
中注册我们的 API
- 在 Azure
中公开我们的 API
- 更新我们的 API 清单
- 添加额外的配置元素
- 添加新包引用
- 更新API项目源代码
我已经完成了#1 和#2,当我查看我的 API 时,我看到:
Display name: CommandAPI_DEV
Application (client) ID: 1e994557-5ae1-47bf-8ab7-b0ce2f8f3852
Object ID: b8225518-eba3-4a6a-8c30-ae82095a4ba7
Directory (tenant) ID: 9f9fbb85-6a89-4fac-a52a-845135fbe887
Application ID URI: api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852
Managed application in local directory: CommandAPI_DEV
对于#3,我已经添加到清单中:
"appRoles": [
{
"allowedMemberTypes": [
"Application"
],
"description": "Daemon apps in this role can consume the web api.",
"displayName": "DaemonAppRole",
"id": "be111a2a-ea62-47a9-8f55-5d8f84af3276",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "DaemonAppRole"
}
],
对于 #4,我创建了以下用户机密:
{
"UserID": "cmddbuser",
"TenantId": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"ResourceId": "app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852",
"Password": "pa55w0rd!",
"Instance": "https://login.microsoftonline.com/",
"Domain": "jdegejdege.onmicrosoft.com",
"ClientId": "1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
}
#5 只是添加 NuGet 包。对于 #6,我已经添加到 ConfigureServices():
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Audience = Configuration["ResourceId"];
opt.Authority = $"{Configuration["Instance"]}{Configuration["TenantId"]}";
})
;
注意这个opt.Audience到:
"app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
和opt.Authority到:
"https://login.microsoftonline.com/9f9fbb85-6a89-4fac-a52a-845135fbe887"
这些值存储在用户机密中,但从 Azure AD 上应用程序页面的“基本”部分复制而来。
然后我添加到 Configure():
app.UseAuthentication();
app.UseAuthorization();
并且我已将 [Authorize]
添加到我的控制器端点之一。
综上所述,API 项目一切正常。
构建客户端:
本章列出了以下步骤:
- 在 Azure AD 中注册客户端应用程序
- 在 Azure
中创建一个客户端密码
- 配置客户端API权限
- 编写我们的客户端应用程序
所以我做了#1,当我在 Azure AD 中查看应用程序时,我看到:
Display name: CommandAPI_Client_DEV
Application (client) ID: d32007a5-642d-413a-82d9-4761e3030890
Object ID: 94d13c40-5556-4e12-aa2c-be9619681712
Directory (tenant) ID: 9f9fbb85-6a89-4fac-a52a-845135fbe887
Application ID URI: Add an Application ID URI
Managed application in local directory: CommandAPI_Client_DEV
然后我做了#2,创建了一个客户端密钥。当我这样做时,UI 显示了三个字段:
Description: CommandAPI_Client_DEV_secret
Value: NTK*****************************
Secret ID: 954fa788-c8b8-4265-97d7-a1bd83be3bcf
(当我创建秘密时,它显示了 37 个字符。当我返回时,我只看到前三个后跟星号。)
然后对于 #3,我进入了 API 权限,向我的 API 添加了一个新权限,然后授予管理员同意。
我没有收到 Microsoft 身份验证弹出窗口,但 Azure UI 将权限状态显示为“已授予默认目录”。
对于#4,我创建了一个简单的控制台应用程序,将其放入 appsettings.json:
{
"Instance": "https://login.microsoftonline.com/{0}",
"TenantId": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"ClientId": "d32007a5-642d-413a-82d9-4761e3030890",
"ClientSecret": "NTK*****************************",
"BaseAddress": "https://localhost:5001/api/Commands/1",
"ResourceId": "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852/.default"
}
请注意,“TenantId”与 Azure AD 中客户端应用程序配置中的“目录(租户)ID”相匹配。
“ClientId”与 Azure AD 中客户端应用程序配置中的“应用程序(客户端)ID”相匹配。
“ClientSecret”与我创建密钥时提供的值字段匹配。
并且“ResourceId”与 Azure AD 中 API 配置中的“应用程序 ID URI”字段相匹配。
客户端应用程序非常简单。
主要调用 RunAsync():
static void Main(string[] args)
{
Console.WriteLine("Making the call...");
RunAsync().GetAwaiter().GetResult();
}
private static async Task RunAsync()
{
AuthConfig 是我们在 appsettings.json 中的设置的简单包装器。 config.Authority 是
AuthConfig config = AuthConfig.ReadFromJsonFile("appsettings.json");
var authority = String.Format(CultureInfo.InstalledUICulture, config.Instance, config.TenantId);
Console.WriteLine($"Authority: {authority}");
我们调用 Azure AD 来获取我们的 JWT:
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder
.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(authority))
.Build();
string[] ResourceIds = new string[] { config.ResourceId };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(ResourceIds).ExecuteAsync();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Token acquired\n");
Console.WriteLine(result.AccessToken);
Console.ResetColor();
}
catch (MsalClientException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
}
if (!string.IsNullOrEmpty(result.AccessToken))
{
然后我们将 JWT 设置为不记名令牌:
var httpClient = new HttpClient();
var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequestHeaders.Accept == null ||
!defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
并调用我们 API 的安全端点:
HttpResponseMessage response = await httpClient.GetAsync(config.BaseAddress);
if (response.IsSuccessStatusCode)
{
Console.ForegroundColor = ConsoleColor.Green;
string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Failed to call the Web API: {response.StatusCode} ");
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Content: {content}");
}
Console.ResetColor();
}
}
当我这样做时,我得到一个有效的 JWT,它解码为:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
"kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}.{
"aud": "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852",
"iss": "https://sts.windows.net/9f9fbb85-6a89-4fac-a52a-845135fbe887/",
"iat": 1636246957,
"nbf": 1636246957,
"exp": 1636250857,
"aio": "E2ZgYLhnIvUl2+2Z4Jno3ebX7pVvAAA=",
"appid": "d32007a5-642d-413a-82d9-4761e3030890",
"appidacr": "1",
"idp": "https://sts.windows.net/9f9fbb85-6a89-4fac-a52a-845135fbe887/",
"oid": "cc6633e2-df64-41be-bbb2-de750766e40a",
"rh": "0.AUYAhbufn4lqrE-lKoRRNfvoh6UHINMtZDpBgtlHYeMDCJCAAAA.",
"roles": [
"DaemonAppRole"
],
"sub": "cc6633e2-df64-41be-bbb2-de750766e40a",
"tid": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"uti": "tgVrQAPepkiMGHO04uHdAA",
"ver": "1.0"
}.[Signature]
但是当我针对 API、httpClient.GetAsync() returns 401 进行调用时,WwwAuthenticate header 设置为:
{
Bearer error="invalid_token",
error_description="The audience 'api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852' is invalid"
}
显然我做错了什么,但是什么?
问题似乎是令牌问题将 aud
字段设置为 "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
与您的安全 API 对观众的期望不匹配,这是 "app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
.
您需要在构建安全 API 的第 4 步中将受众设置为 api...
才能正常工作。
我正在研究 Les Jackson 的 The Complete ASP.NET Core 3 API Tutorial,但我被卡住了。我在第 14 章“保护我们的 API”。
在本章中,我们应该做的是配置我们构建的 API 以接受来自 Azure Active Directory 的 JWT 令牌,然后构建一个获取 JWT 令牌的客户端来自 Azure Active Directory,然后将其作为自定义 header 包含在针对 API.
的调用中此时,我们仍然是 运行 本地主机上的 API,接下来是在 Azure 上托管它。
在所有这些中,我使用的是 Azure 中的默认目录。
将 Azure AD JWT 添加到 API:
本章列出了以下步骤:
- 在 Azure AD 中注册我们的 API
- 在 Azure 中公开我们的 API
- 更新我们的 API 清单
- 添加额外的配置元素
- 添加新包引用
- 更新API项目源代码
我已经完成了#1 和#2,当我查看我的 API 时,我看到:
Display name: CommandAPI_DEV
Application (client) ID: 1e994557-5ae1-47bf-8ab7-b0ce2f8f3852
Object ID: b8225518-eba3-4a6a-8c30-ae82095a4ba7
Directory (tenant) ID: 9f9fbb85-6a89-4fac-a52a-845135fbe887
Application ID URI: api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852
Managed application in local directory: CommandAPI_DEV
对于#3,我已经添加到清单中:
"appRoles": [
{
"allowedMemberTypes": [
"Application"
],
"description": "Daemon apps in this role can consume the web api.",
"displayName": "DaemonAppRole",
"id": "be111a2a-ea62-47a9-8f55-5d8f84af3276",
"isEnabled": true,
"lang": null,
"origin": "Application",
"value": "DaemonAppRole"
}
],
对于 #4,我创建了以下用户机密:
{
"UserID": "cmddbuser",
"TenantId": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"ResourceId": "app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852",
"Password": "pa55w0rd!",
"Instance": "https://login.microsoftonline.com/",
"Domain": "jdegejdege.onmicrosoft.com",
"ClientId": "1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
}
#5 只是添加 NuGet 包。对于 #6,我已经添加到 ConfigureServices():
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Audience = Configuration["ResourceId"];
opt.Authority = $"{Configuration["Instance"]}{Configuration["TenantId"]}";
})
;
注意这个opt.Audience到:
"app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
和opt.Authority到:
"https://login.microsoftonline.com/9f9fbb85-6a89-4fac-a52a-845135fbe887"
这些值存储在用户机密中,但从 Azure AD 上应用程序页面的“基本”部分复制而来。
然后我添加到 Configure():
app.UseAuthentication();
app.UseAuthorization();
并且我已将 [Authorize]
添加到我的控制器端点之一。
综上所述,API 项目一切正常。
构建客户端:
本章列出了以下步骤:
- 在 Azure AD 中注册客户端应用程序
- 在 Azure 中创建一个客户端密码
- 配置客户端API权限
- 编写我们的客户端应用程序
所以我做了#1,当我在 Azure AD 中查看应用程序时,我看到:
Display name: CommandAPI_Client_DEV
Application (client) ID: d32007a5-642d-413a-82d9-4761e3030890
Object ID: 94d13c40-5556-4e12-aa2c-be9619681712
Directory (tenant) ID: 9f9fbb85-6a89-4fac-a52a-845135fbe887
Application ID URI: Add an Application ID URI
Managed application in local directory: CommandAPI_Client_DEV
然后我做了#2,创建了一个客户端密钥。当我这样做时,UI 显示了三个字段:
Description: CommandAPI_Client_DEV_secret
Value: NTK*****************************
Secret ID: 954fa788-c8b8-4265-97d7-a1bd83be3bcf
(当我创建秘密时,它显示了 37 个字符。当我返回时,我只看到前三个后跟星号。)
然后对于 #3,我进入了 API 权限,向我的 API 添加了一个新权限,然后授予管理员同意。
我没有收到 Microsoft 身份验证弹出窗口,但 Azure UI 将权限状态显示为“已授予默认目录”。
对于#4,我创建了一个简单的控制台应用程序,将其放入 appsettings.json:
{
"Instance": "https://login.microsoftonline.com/{0}",
"TenantId": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"ClientId": "d32007a5-642d-413a-82d9-4761e3030890",
"ClientSecret": "NTK*****************************",
"BaseAddress": "https://localhost:5001/api/Commands/1",
"ResourceId": "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852/.default"
}
请注意,“TenantId”与 Azure AD 中客户端应用程序配置中的“目录(租户)ID”相匹配。
“ClientId”与 Azure AD 中客户端应用程序配置中的“应用程序(客户端)ID”相匹配。
“ClientSecret”与我创建密钥时提供的值字段匹配。
并且“ResourceId”与 Azure AD 中 API 配置中的“应用程序 ID URI”字段相匹配。
客户端应用程序非常简单。
主要调用 RunAsync():
static void Main(string[] args)
{
Console.WriteLine("Making the call...");
RunAsync().GetAwaiter().GetResult();
}
private static async Task RunAsync()
{
AuthConfig 是我们在 appsettings.json 中的设置的简单包装器。 config.Authority 是
AuthConfig config = AuthConfig.ReadFromJsonFile("appsettings.json");
var authority = String.Format(CultureInfo.InstalledUICulture, config.Instance, config.TenantId);
Console.WriteLine($"Authority: {authority}");
我们调用 Azure AD 来获取我们的 JWT:
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder
.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(authority))
.Build();
string[] ResourceIds = new string[] { config.ResourceId };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(ResourceIds).ExecuteAsync();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Token acquired\n");
Console.WriteLine(result.AccessToken);
Console.ResetColor();
}
catch (MsalClientException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
}
if (!string.IsNullOrEmpty(result.AccessToken))
{
然后我们将 JWT 设置为不记名令牌:
var httpClient = new HttpClient();
var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequestHeaders.Accept == null ||
!defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
并调用我们 API 的安全端点:
HttpResponseMessage response = await httpClient.GetAsync(config.BaseAddress);
if (response.IsSuccessStatusCode)
{
Console.ForegroundColor = ConsoleColor.Green;
string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Failed to call the Web API: {response.StatusCode} ");
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Content: {content}");
}
Console.ResetColor();
}
}
当我这样做时,我得到一个有效的 JWT,它解码为:
{
"typ": "JWT",
"alg": "RS256",
"x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
"kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}.{
"aud": "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852",
"iss": "https://sts.windows.net/9f9fbb85-6a89-4fac-a52a-845135fbe887/",
"iat": 1636246957,
"nbf": 1636246957,
"exp": 1636250857,
"aio": "E2ZgYLhnIvUl2+2Z4Jno3ebX7pVvAAA=",
"appid": "d32007a5-642d-413a-82d9-4761e3030890",
"appidacr": "1",
"idp": "https://sts.windows.net/9f9fbb85-6a89-4fac-a52a-845135fbe887/",
"oid": "cc6633e2-df64-41be-bbb2-de750766e40a",
"rh": "0.AUYAhbufn4lqrE-lKoRRNfvoh6UHINMtZDpBgtlHYeMDCJCAAAA.",
"roles": [
"DaemonAppRole"
],
"sub": "cc6633e2-df64-41be-bbb2-de750766e40a",
"tid": "9f9fbb85-6a89-4fac-a52a-845135fbe887",
"uti": "tgVrQAPepkiMGHO04uHdAA",
"ver": "1.0"
}.[Signature]
但是当我针对 API、httpClient.GetAsync() returns 401 进行调用时,WwwAuthenticate header 设置为:
{
Bearer error="invalid_token",
error_description="The audience 'api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852' is invalid"
}
显然我做错了什么,但是什么?
问题似乎是令牌问题将 aud
字段设置为 "api://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
与您的安全 API 对观众的期望不匹配,这是 "app://1e994557-5ae1-47bf-8ab7-b0ce2f8f3852"
.
您需要在构建安全 API 的第 4 步中将受众设置为 api...
才能正常工作。