This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.
Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular
This post is going to take the solution from last week, the code can be found here, and add an example of the Client Application (Angular) calling an endpoint on the API Application that requires a user with permissions.
API Application
To provide an endpoint to call with minimal changes this example just moves the SampleDataController from the Client Application to the API Application. The following is the full class.
using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace WebApplicationBasic.Controllers { [Route("api/[controller]")] [Authorize] public class SampleDataController : Controller { private static string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; [HttpGet("[action]")] public IEnumerable<WeatherForecast> WeatherForecasts() { var rng = new Random(); return Enumerable.Range(1, 5) .Select(index => new WeatherForecast { DateFormatted = DateTime.Now.AddDays(index).ToString("d"), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }); } public class WeatherForecast { public string DateFormatted { get; set; } public int TemperatureC { get; set; } public string Summary { get; set; } public int TemperatureF { get { return 32 + (int)(TemperatureC / 0.5556); } } } } }
Make special note that this class now has the Authorize attribute applied which is the only change that was made when moving the file from the Client Application. This attribute is what will require an authorized user for all the routes this controller services.
Client Application
In the Client Application, the first step is to remove the SampleDataController since it is now in the API Application.
Next, in the app.module.client.ts file, add a new provider which can be used to supply the URL of the API to the rest of the Client Application. Don’t take this as best practices for injecting configuration data it is just an easy way to handle it in this application. The following is the full class without the imports (which haven’t changed) the new item is the API_URL.
@NgModule({ bootstrap: sharedConfig.bootstrap, declarations: sharedConfig.declarations, imports: [ BrowserModule, FormsModule, HttpModule, ...sharedConfig.imports ], providers: [ { provide: 'ORIGIN_URL', useValue: location.origin }, { provide: 'API_URL', useValue: "http://localhost:5001/api/" }, AuthService, AuthGuardService, GlobalEventsManager ] }) export class AppModule { }
Now for the changes that need to be made to the FetchDataComponent which is the class that will call the new API endpoint. First, add an import for the AuthService.
import { AuthService } from '../services/auth.service';
Next, there are a couple of changes to the signature of the constructor. The first is to use ‘API_URL’ instead of ‘ORIGIN_URL’. The second is to provide for injection of the AuthService. The following is a comparison between the version of the constructor signature.
Before: constructor(http: Http, @Inject('ORIGIN_URL') originUrl: string) After: constructor(http: Http, @Inject('API_URL') apiUrl: string, authService: AuthService)
The final change is to use authService.AuthGet with the new URL instead of http.get.
Before: http.get(originUrl + '/api/SampleData/WeatherForecasts').subscribe(result => { this.forecasts = result.json() as WeatherForecast[]; }); After: authService.AuthGet(apiUrl + '/SampleData/WeatherForecasts').subscribe(result => { this.forecasts = result.json() as WeatherForecast[]; });
With the above changes, the user has to be logged in or the API will respond with not authorized for the weather forecasts end point. The Client Application doesn’t have anything to provide the user with the fact they aren’t authorized at the moment, but that is outside the scope of this entry.
So far we haven’t look at the code in the AuthService class, but I do want to explain what the AuthGet function is doing and the related functions for put, delete, and post. These calls are wrappers around the standard Angular HTTP library calls that add authorization headers based on the logged in user. The following is the code of the AuthGet as well as two helper functions the class uses to add the headers.
AuthGet(url: string, options?: RequestOptions): Observable<Response> { if (options) { options = this._setRequestOptions(options); } else { options = this._setRequestOptions(); } return this.http.get(url, options); } private _setAuthHeaders(user: User) { this._authHeaders = new Headers(); this._authHeaders.append('Authorization', user.token_type + " " + user.access_token); this._authHeaders.append('Content-Type', 'application/json'); } private _setRequestOptions(options?: RequestOptions) { if (options) { options.headers.append(this._authHeaders.keys[0], this._authHeaders.values[0]); } else { //setting default authentication headers this._setAuthHeaders(this._currentUser); options = new RequestOptions({ headers: this._authHeaders, body: "" }); } return options; }
Wrapping up
It feels like this application is finally getting to the point where other development could happen if it were more than a demo, which is exciting. My thought on how this could be used for real applications is the Identity Application would stand on its own and be used by many clients. The Client Application with a few more tweaks could be used as a template for Angular applications. The completed code can be found here.
This post finishes up the core of what I set out to learn about IdentityServer, but there could be more related posts as I continue to add some polish to the current implementation of the sample solution.
Also published on Medium.