.NET Core WebAPI + OpenIdDict(凭据流)和Angular2客户端:401成功登录后(完全重新生成)

时间:2016-11-19 09:22:19

标签: asp.net authentication angular asp.net-web-api openiddict

我尝试使用OpenIdDict创建一个使用凭据流的.NET Core Web API的Angular2 SPA。在为这个问题创建一个repro解决方案时,我还在自述文件中详细说明了我的所有步骤,所以希望这篇文章对像我这样的新手有用。请在这些存储库中找到完整的repro解决方案

至于服务器端,我按照OpenIdDict提供的关于此流程的示例(https://github.com/openiddict/openiddict-samples/blob/master/samples/PasswordFlow)。以下是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,它会按预期返回JSON数据。

示例令牌请求:

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(参见下面的更多日志条目)。

检索某些资源的服务是这样的:

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只获取表示Headerhttps://angular.io/docs/ts/latest/api/http/index/Headers-class.html)条目的某些属性,检索存储的令牌,将Authentication条目附加到标头,然后返回它:

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;
}

然而,当尝试将响应映射到JSON对象(Unexpected end of JSON input)时,此请求获得401响应然后Angular抛出。

我必须补充一点,一旦客户端获取令牌,它就会发出另一个请求,以检索用户信息,这样可以正常工作;这是它(见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);
        });
    }

然而,使用上述服务构建的任何其他请求都会失败。使用引用函数构建的标题有什么问题?

以下是服务器端的一些日志条目:

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. 

1 个答案:

答案 0 :(得分:4)

您的不记名令牌使用不正确。

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