.NET Core WebAPI + OpenIdDict(凭证流)和Angular2客户端:成功登录(完全复制)后的401 [英] .NET Core WebAPI + OpenIdDict (credentials flow) and Angular2 client: 401 after successful login (full repro)
问题描述
我正在尝试创建一个Angular2 SPA,该SPA使用带有OpenIdDict和凭据流保护的.NET Core Web API.在创建此问题的复制解决方案时,我还详细介绍了自述文件中的所有步骤,因此希望本文对像我这样的新手有用.请在以下存储库中找到完整复制解决方案:
I'm trying to create an Angular2 SPA consuming a .NET Core Web API protected using OpenIdDict, with credentials flow. In creating a repro solution for this issue, I have also detailed all my steps in a readme, so hope this post can be useful to newbies like me. Please find the full repro solutions in these repositories:
-
服务器端(.NET Core + OpenIdDict),其中包含建立自己的详细说明: https://github.com/Myrmex/repro-oidang
客户端(Angular2): https://github.com/Myrmex/repro-心绞痛
client-side (Angular2): https://github.com/Myrmex/repro-angoid
对于服务器端,我遵循了OpenIdDict提供的有关此流程的示例(
As for the server side, I followed the sample provided by OpenIdDict about this flow (https://github.com/openiddict/openiddict-samples/blob/master/samples/PasswordFlow). Here are the most relevant bits in Startup
:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddEntityFrameworkSqlServer()
.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Catalog")))
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Catalog")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddOpenIddict<ApplicationDbContext>()
.DisableHttpsRequirement()
.EnableTokenEndpoint("/connect/token")
.EnableLogoutEndpoint("/connect/logout")
.EnableUserinfoEndpoint("/connect/userinfo")
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.AddEphemeralSigningKey();
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
});
// add my services
// ...
services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();
services.AddSwaggerGen();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory,
IDatabaseInitializer databaseInitializer)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddNLog();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCors(builder =>
builder.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod());
app.UseOAuthValidation();
app.UseOpenIddict();
app.UseMvc();
databaseInitializer.Seed().GetAwaiter().GetResult();
env.ConfigureNLog("nlog.config");
app.UseSwagger();
app.UseSwaggerUi();
}
如果我用Fiddler对其进行测试,则可以正常工作:令牌请求获得令牌,然后可以将其包含在Authorization
标头中以访问任何受保护的API,该API将按预期返回JSON数据.
If I test it with Fiddler, it works fine: the token request gets the token, and I can then include it in the Authorization
header to access any protected API, which returns JSON data as expected.
样本令牌请求:
POST http://localhost:51346/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&scope=offline_access profile email roles&resource=http://localhost:4200&username=...&password=...
样本资源请求:
GET http://localhost:51346/api/values
Content-Type: application/json
Authorization: Bearer ...received token here...
但是,在客户端,每当我尝试相同的请求时,我都会收到401错误;查看日志,似乎Angular2 Http服务根本没有发送所需的标头,因为出现错误Authentication was skipped because no bearer token was received
(请参阅下面的更多日志条目).
Yet, on the client side, whenever I try the same request I get a 401 error; looking at the log, it seems that Angular2 Http service is not sending the required header at all, as I get the error Authentication was skipped because no bearer token was received
(see more log entries below).
检索某些资源的服务是这样的:
A service retrieving some resources is like this:
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { SettingsService } from './settings.service';
import { AuthenticationService } from './authentication.service';
export interface ICategory {
id: string;
name: string;
}
@Injectable()
export class CategoryService {
constructor(
private _http: Http,
private _settings: SettingsService,
private _authService: AuthenticationService) { }
public getCategories(): Observable<ICategory[]> {
let url = this._settings.apiBaseUrl + 'categories';
let options = {
headers: this._authService.createAuthHeaders({
'Content-Type': 'application/json'
})
};
return this._http.get(url, options).map((res: Response) => res.json())
.catch((error: any) => Observable.throw(error.json().error || 'server error'));
}
}
帮手createAuthHeaders
仅获得表示Header
的一些属性( https://angular.io/docs/ts/latest/api/http/index/Headers-class.html )条目,检索存储的令牌,将Authentication
条目附加到标题,并返回它:
Where the helper createAuthHeaders
just gets some properties representing the Header
(https://angular.io/docs/ts/latest/api/http/index/Headers-class.html) entries, retrieves the stored token, appends the Authentication
entry to the header, and returns it:
public createAuthHeaders(headers?: { [name: string]: any }): Headers {
let auth = new Headers();
if (headers) {
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
auth.append(key, headers[key]);
}
}
}
let tokenResult = this._localStorage.retrieve(this._settings.tokenStorageKey, true);
if (tokenResult) {
auth.append('Authentication', 'Bearer ' + tokenResult.access_token);
}
return auth;
}
但是,此请求获得401响应,然后尝试将响应映射到JSON对象(Unexpected end of JSON input
)时Angular引发.
Yet, this request gets a 401 response and then Angular throws when trying to mapping the response to a JSON object (Unexpected end of JSON input
).
我必须补充一点,即客户端获得令牌后,它将立即发出另一个请求以检索用户信息,这很好用;就是这里(请参阅get user info
之后的代码):
I must add that as soon as the client gets the token, it makes another request with it, to retrieve user info, and this works fine; here is it (see the code after get user info
):
public login(name: string, password: string) {
let body = 'grant_type=password&scope=offline_access profile email roles' +
`&resource=${this._settings.appBaseUrl}&username=${name}&password=${password}`;
this._http.post(
this._settings.authBaseUrl + `token`,
body,
{
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
}).map(res => res.json())
.subscribe(
(token: ITokenResult) => {
if (token.expires_in) {
token.expires_on = this.calculateExpirationDate(+token.expires_in);
}
this._localStorage.store(this._settings.tokenStorageKey, token, true);
// get user info
this._http.get(this._settings.authBaseUrl + 'userinfo', {
headers: new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token.access_token
})
}).map(res => res.json())
.subscribe((info: IUserInfoResult) => {
let user: IUser = {
id: info.name,
email: info.email,
name: info.name,
firstName: info.given_name,
lastName: info.family_name,
role: info.role,
verified: info.email_verified
};
this._localStorage.store(this._settings.userStorageKey, user, true);
this.userChanged.emit(user);
}, error => {
console.log(error);
});
},
error => {
console.log(error);
});
}
但是,使用上述服务建立的任何其他请求均失败.用带引号的函数构建的标题有什么问题?
Yet, any other request, built using the service above, fails. What's wrong with the headers built with the quoted function?
以下是服务器端的一些日志条目:
Here are some log entries on the server side:
2016-11-18 20:41:31.9815|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
2016-11-18 20:41:31.9815|0|OpenIddict.Infrastructure.OpenIddictProvider|INFO| The token request validation process was skipped because the client_id parameter was missing or empty.
2016-11-18 20:41:32.0715|0|AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware|INFO| No explicit audience was associated with the access token.
2016-11-18 20:41:32.1165|10|AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware|INFO| AuthenticationScheme: ASOS signed in.
2016-11-18 20:41:32.1635|3|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
2016-11-18 20:41:57.7430|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
2016-11-18 20:41:57.7430|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
2016-11-18 20:41:57.8820|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
2016-11-18 20:41:57.9305|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
2016-11-18 20:41:57.9465|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
2016-11-18 20:41:57.9925|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
推荐答案
您的承载令牌用法不正确.
Your bearer token usage is incorrect.
auth.append('Authentication', 'Bearer ' + tokenResult.access_token) // wrong
auth.append('Authorization', 'Bearer ' + tokenResult.access_token) // right
标题必须为Authorization
.参见 https://tools.ietf.org/html/rfc6750#section-2.1
这篇关于.NET Core WebAPI + OpenIdDict(凭证流)和Angular2客户端:成功登录(完全复制)后的401的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!