7

Ich versuche, eine Angular2 SPA eine .NET Core Web API zu erstellen, die mit OpenIdDict geschützt ist, mit Anmeldeinformationen fließen. Bei der Erstellung einer Repro-Lösung für dieses Problem habe ich auch alle meine Schritte in einer Readme-Datei beschrieben. Hoffentlich kann dieser Beitrag für Neulinge wie mich nützlich sein. Bitte finden Sie die voll Repro Lösungen in diesen Repositories:.NET Core WebAPI + OpenIdDict (Anmeldeinformationen fließen) und Angular2-Client: 401 nach erfolgreicher Anmeldung (vollständige Repro)

Wie für die Server-Seite, gefolgt von der I OpenIdDict dieser Strömung (https://github.com/openiddict/openiddict-samples/blob/master/samples/PasswordFlow) vorgesehen Probe. Hier sind die wichtigsten 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(); 
} 

Wenn ich es mit Fiddler testen, es funktioniert gut: die Tokenanforderung das Token erhält, und ich kann dann schließen sie in den Authorization Header jede geschützte API zugreifen zu können, Das gibt JSON-Daten wie erwartet zurück.

Muster Token Anfrage:

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=... 

Beispielressourcenanforderung:

GET http://localhost:51346/api/values 
Content-Type: application/json 
Authorization: Bearer ...received token here... 

Doch auf der Client-Seite, wenn ich den gleichen Wunsch versuchen bekomme ich einen 401-Fehler; Mit Blick auf das Protokoll scheint Angular2 Http-Dienst die erforderliche Header überhaupt nicht zu senden, da ich den Fehler Authentication was skipped because no bearer token was received (siehe mehr Protokolleinträge unten) erhalten.

Ein Dienst einige Ressourcen Abrufen das ist wie:

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

Wo der Helfer createAuthHeaders bekommt nur einige Eigenschaften, die die Header (https://angular.io/docs/ts/latest/api/http/index/Headers-class.html) Einträge, ruft die gespeicherten Token, hängt den Authentication Eintritt in den Header und es gibt:

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

Doch diese Anfrage erhält eine 401-Antwort und dann Angular wirft, wenn sie die Abbildung der Antwort auf ein JSON-Objekt versucht (Unexpected end of JSON input).

Ich muss hinzufügen, dass, sobald der Client das Token erhält, es eine weitere Anfrage mit ihm macht, um Benutzerinformationen abzurufen, und das funktioniert gut; hier ist es (den Code nach get user info sehen):

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

Doch jede andere Anfrage, baute den Service oben verwenden, schlägt fehl. Was ist falsch an den Headern, die mit der Funktion in Anführungszeichen erstellt wurden?

Hier sind einige Protokolleinträge auf der Serverseite:

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. 
+0

Haben Sie dieses Projekt kürzlich aktualisiert? Es gibt eine neue Version von OpenIdDict, und Ihr Beispiel funktioniert nicht mehr. Es scheitert innerhalb der DatabaseInitializer.Seed(), da der ApplicationDbContext keine _context.Applications.Any() mehr hat ... – marrrschine

+0

Guys, ich habe gerade das Sample-Repository aktualisiert, damit es mit dem neu veröffentlichten OpenIdDict kompatibel ist. Bitte überprüfen Sie es, ich habe nicht in einer realen App getestet, aber das Beispiel scheint wieder zu funktionieren. Ich habe die Readme-Datei entsprechend aktualisiert, aber überprüfe den Quellcode, da ich etwas vergessen habe. – Naftis

+0

wuah das war schnell! vielen Dank. Leider habe ich eine Menge dummer Fragen. Welche Art von Informationen können Sie in den Geltungsbereich aufnehmen? Zu welchem ​​Zweck verwenden Sie UserSecrets? Wäre es eine große Sache, zu JWT zu wechseln? Wie kann ich dieses Projekt als eigenständige EXE ausführen? Nochmals vielen Dank dude – marrrschine

Antwort

3

Ihre Inhaber Token Nutzung nicht korrekt ist.

auth.append('Authentication', 'Bearer ' + tokenResult.access_token) // wrong 
auth.append('Authorization', 'Bearer ' + tokenResult.access_token) // right 

Die Kopfzeile muss Authorization sein. Siehe https://tools.ietf.org/html/rfc6750#section-2.1

+0

Wow, kann nicht glauben, dass ich so blind war. Hab so viele Stunden damit verbracht, das Ganze aufzustellen, und habe nicht bemerkt, dass ich ein Wort für ein anderes ausgetauscht habe :). Wie auch immer, ich hoffe, dass dies nützlich für diejenigen sein wird, die nach Auth-Samples mit .NET-Kern und Angular2 suchen ... – Naftis

+0

@Naftis Das erinnert an die 32 Stunden, die ich brauchte, um einen Fehler zu finden, der bei der Verwendung von HTTP für eine OpenID Connect passiert ist Anfrage und HTTPS für die Antwort. –

+0

danke für das Teilen! – marrrschine

Verwandte Themen