Now for the final part of the Quest for Identity posts, how to configure an API to work with Identity Server 4.
RESTful Service configuration
Firstly let's create a new ASP.Net Web application project within our solution. Make sure it's a created with the Web API template so it can act as a RESTful service. I've named mine IS4REST. So how can we now implement IS4 Authentication within our project?
Surprisingly this is the easiest stage of the IS4 configuration process. Within the Startup class in ConfigureServices(), add in the following CORS configuration and authentication with your own port configurations:
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin();
});
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:44392/";
// name of the API resource
options.Audience = "https://localhost:44392/resources";
});
Then enable them in the Cofiguration() call below:
app.UseCors("default");
app.UseAuthentication();
Finally, since this is a new project, our Identity Server needs to know about this as a client setting. Add the following to the clients' section of the Identity Server 4 Config file:
new Client
{
ClientId = "rest",
ClientName = "REST API",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
AllowAccessTokensViaBrowser = true,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"https://localhost:44305/signin-oidc"},
PostLogoutRedirectUris = {"https://localhost:44305/signout-callback-oidc"},
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
}
This is exactly the same as our MVC configuration except it has a different ID and we are Allowing Access Tokens through a Browser. That's all we need to do this side, over to the Angular set up using the project from the previous post.
Angular Client Set Up
We're going to need a new page and service to call our new API Service run the following:
ng g c components/api-test
ng g s shared/services/api-test
Register the component in the app module and the service in the shared providers. Then we need to fill them in with the following:
#api-test-component.ts
import { Component, OnInit } from '@angular/core';
import { ApiTestService } from '../../shared/services/api-test.service';
@Component({
selector: 'ac-api-test',
templateUrl: './api-test.component.html',
styleUrls: ['./api-test.component.css']
})
export class ApiTestComponent implements OnInit {
response: any = null;
constructor(private _apiTestService: ApiTestService) { }
ngOnInit() {
this._apiTestService.GetValues().subscribe(response => {
this.response = response;
});
}
#api-test-component.html
<p>
api-test works!
</p>
{{response}}
#api-test-service
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
@Injectable()
export class ApiTestService {
apiRoot : string = "https://localhost:44329/api/"
constructor(private _http : HttpClient) { }
GetValues(): Observable<any> {
return this._http.get(this.apiRoot + "values")
.do(data => {
})
.catch(this.handleError);
}
private handleError(err: HttpErrorResponse) {
console.log(err.message);
return Observable.throw(err.message);
}
}
Finally to ge tthis into a working state, lets add this new component to the router and provide a link to it on the app.component.html:
#app.component.html
<h3><a [routerLink]="['/']">Home</a> | <a [routerLink]="['/protected']">Protected</a> | <a [routerLink]="['/api-test']">API Test</a></h3>
<h1>
{{title}}
</h1>
<router-outlet></router-outlet>
#app-route-module.ts
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'protected',
component: ProtectedComponent,
canActivate: [AuthGuardService]
},
{
path: 'api-test',
component: ApiTestComponent,
canActivate: [AuthGuardService]
},
{
path: 'auth-callback',
component: AuthCallbackComponent
}
Now this will work! We've successfully configured our application to call our new RESTful service. However, there is a but. We don't currently require Authorization on our api/values route. You should enable it with the [Authorize] property on the Values API class and see what happens. As expected a 401. To get it working we need to pass back our Identity Token.
Setting up an Interceptor
Now the best way to pass back our token is to use an interceptor because in general practice you'll be providing the token to multiple API calls. Our intercept will spot all API calls and ad the token to the header before sending it on its way.
Firstly add the following package to our solution.
yarn add angular2-jwt
Next up let's add some token functionality to our AuthService, they should be quite clear on what they do:
#auth.service.ts
...
public getToken(): string {
var token = localStorage[0];
return token;
}
public isAuthenticated(): boolean {
// get the token
const token = this.getToken();
// return a boolean reflecting
// whether or not the token is expired
return tokenNotExpired(null, token);
}
public collectFailedRequest(request): void {
this.cachedRequests.push(request);
}
public retryFailedRequests(): void {
// retry the requests. this method can
// be called after the token is refreshed
}
With that in place, lets add our interceptors. Create new folder called interceptors and then add two files (manual process I'm afraid), jwt.interceptor.ts and token.interceptor.ts. The code for these two files is as follows:
#jwt.interceptor.ts
import 'rxjs/add/operator/do';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { AuthService } from '../shared/services/auth.service';
import { Observable } from 'rxjs/Observable';
export class JwtInterceptor implements HttpInterceptor {
constructor(public auth: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
{
return next.handle(request).do((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do stuff with response if you want
}
},
(err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.auth.collectFailedRequest(request);
}
}
});
}
}
#token.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { AuthService } from '../shared/services/auth.service';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(public auth: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
Authorization: this.auth.getAuthorizationHeaderValue()
}
});
return next.handle(request);
}
}
Have a look through the code and have a look at home it manages to add the Authentication Token to our requests. We have one final thing to do, let's add the interceptor to our app.module as a provider and import the relevant files:
#app.module.ts
Providers : [{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}]
Give it another go with some breakpoints to see what happens but we now have our API working alongside our client. That is where I'll leave these posts now, there are a lot more areas to explore but, I hope the last three posts have provided enough information to help you hit the ground running. Cheers for reading.
The code for this here
Comments