If you haven’t read the previous parts of this series:
Background – Angular – Keycloak blog series Part 5
Welcome to part 5 of the blog series, "Integrating Keycloak with an Angular 4 web application"
. Before we continue where we left off in part 4 of the series, let’s do a quick recap of part 4 as well as what we’ll cover in this part of the series.
In part 4, we installed the Twitter Bootstrap framework as a dependency by making use of npm
commands in either a Command Prompt
window (if you’re using Microsoft Windows) or a Terminal window
(if you’re using any *nix based OS). We also installed the Keycloak JavaScript adapter as a dependency, made sure that both the Bootstrap and Keycloak JavaScript adapter are correctly imported in the .angular-cli.json
file, as well as discussed the ng serve
command, which we’ll use to compile and serve the web application on the local machine. At this point you should have two newly created TypeScript classes, namely: keycloak.http.ts
and keycloak.service.ts
.
Project Code Link
If you want to download the project to reference the code base you can do so simply by navigating to the following link and downloading the project.
https://drive.google.com/open?id=0B4H8V7DA5DrJckd5WjNRVHlIZWM
Keycloak Service TypeScript
Code Part 1
declare let Keycloak: any; @Injectable() export class KeycloakService { static auth: any = {}; }
First declare a new object named Keycloak before the service class declaration. We’ll use this object to set some initial parameters programmatically. If you go to your Keycloak realm you can export configuration settings for that realm in a .json
file format. We’ll bypass this by making use of an object and setting the variables programmatically.
This class will be a service
class, so we need to be able to inject it as a provider in a component later on; in order to do this specify the @Injectable()
annotation before the class declaration.
Code Part 2
static init(): Promise { const keycloakAuth: any = Keycloak({ url: environment.keycloakRootUrl, realm: 'angular_keycloak', clientId: 'client-ui', 'ssl-required': 'external', 'public-client': true }); KeycloakService.auth.loggedIn = false; return new Promise((resolve, reject) => { keycloakAuth.init({onLoad: 'login-required'}) .success(() => { console.log(keycloakAuth); KeycloakService.auth.loggedIn = true; KeycloakService.auth.authz = keycloakAuth; KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl + '/realms/angular_keycloak/protocol/openid-connect/logout?redirect_uri=' + document.baseURI; resolve(); }) .error(() => { reject(); }); }); }
Create a static method called init()
of type Promise (which accepts a variable of type any). This method contains code which handles authentication as well as the environment settings for our application (such as the Keycloak realm, the Client-Id we want to interact with etc.). Please note that your realm and client names could differ from mine; if they do, make sure to use your values where necessary.
Code Part 3
static logout() { console.log('** LOGOUT'); KeycloakService.auth.loggedIn = false; KeycloakService.auth.authz = null; window.location.href = KeycloakService.auth.logoutUrl; }
Create a static method called logout()
. This method contains code which will handle the logout process of the application. It sets the appropriate variables to nullify the active session as well as to redirect the browser to the logoutUrl
location (which is specified in the static init()
method).
Code Part 4
getToken(): Promise { return new Promise((resolve, reject) => { if (KeycloakService.auth.authz.token) { KeycloakService.auth.authz .updateToken(5) .success(() => { resolve(KeycloakService.auth.authz.token); }) .error(() => { reject('Failed to refresh token'); }); } else { reject('Not logged in'); } }); }
Create a new method called getToken()
of type Promise (which accepts a variable of type string). This method contains code which will try to obtain a token from the Keycloak Application Server to authenticate a session and/or http(s) calls. If the current token is older than the specific value set in the code, it will attempt to refresh the current token with a new one for that session.
Code Part 5
static getUsername(): string { return KeycloakService.auth.authz.tokenParsed.preferred_username; } static getFullName(): string { return KeycloakService.auth.authz.tokenParsed.name; }
Create two new static methods called getUsername()
and getFullName()
, both of which are of return type string. The getUsername()
method will retrieve the username value from the current session token. The getFullName()
method will retrieve the full name from the current session token. In the Keycloak realm you can set additional parameters which can be obtained from a valid token.
Keycloak HTTP TypeScript
@Injectable() export class KeycloakHttp extends Http { constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, private keycloakService: KeycloakService) { super(backend, defaultOptions); } request(url: string | Request, options?: RequestOptionsArgs): Observable { const tokenPromise: Promise = this.keycloakService.getToken(); const tokenObservable: Observable = Observable.fromPromise(tokenPromise); if (typeof url === 'string') { return tokenObservable.map(token => { const authOptions = new RequestOptions( {headers: new Headers({'Authorization': 'Bearer ' + token})} ); return new RequestOptions().merge(options).merge(authOptions); }).concatMap(opts => super.request(url, opts)); } else if (url instanceof Request) { return tokenObservable.map(token => { url.headers.set('Authorization', 'Bearer ' + token); return url; }).concatMap(request => super.request(request)); } } } export function keycloakHttpFactory(backend: XHRBackend, defaultOptions: RequestOptions, keycloakService: KeycloakService) { return new KeycloakHttp(backend, defaultOptions, keycloakService); }
Just like the KeycloakService
class the KeycloakHttp
class will be used as an Angular service. Later on in the AppModule
class we’ll import both the KeycloakService
and the KeycloakHttp
classes as providers.
The extends Http
means that we’re extending Angular’s HTTP class (which can be found in the core package).
In a nutshell, the request
method will update any HTTP request that is made, and append a new key to the request headers, namely: "Authorization"
with the value of "Bearer"
followed by a space and the authentication token for the session. If the token is no longer valid the "KeycloakService"
class will attempt to refresh the token by making use of the getToken()
method. In essence this means that if you make an HTTP call to get all the users from the database and you don’t have a valid authorization token, you’ll get an error in return.
Environment Variables
Rather than creating instance variables for URL routes in every class, I’ve opted to make use of the environments
configuration classes. You should have a package named environments
, which contains two classes, namely: environment.prod.ts
(which is invoked when you do a production build) and environment.ts
(which is invoked when you do a development build or "ng serve"
).
Production Environment Configuration
export const environment = { production: true, keycloakRootUrl: 'http://localhost:8080/auth' };
Development Environment Configuration
export const environment = { production: false, keycloakRootUrl: 'http://localhost:8080/auth' };
In this case I won’t be deploying this application to a production instance, so my keycloakRootUrl
variable’s value is exactly the same in both environments. In a normal situation you can declare the API endpoints to be used in the environments classes.
App Module Updates
If you’ve followed along with each part, your app.module.ts
class should look something like this:
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] })
The providers
array is empty at the moment. Because we want to use the Keycloak Service and Keycloak HTTP classes in the global scope of our project we need to specify them as provider
entries in the global app.module.ts
(which in the scope of this tutorial series is coincidentally the only app.module.ts
). Refer to the code block below to see what you need to add.
providers: [ { provide: KeycloakHttp, useFactory: keycloakHttpFactory, deps: [XHRBackend, RequestOptions, KeycloakService] }, KeycloakService, ],
We’ve added two new objects in the providers
array; the first being the exported function in the keycloak.http.ts
class, and the second being the Keycloak Service class.
Entry Point Update – Main Class
The final piece of the puzzle is situated in the main.ts
file (which serves as the entry point for the application). We need to "encapsulate"
the default Angular bootstrap process with our Keycloak Service. The code block below demonstrates how to do this.
KeycloakService.init() .then(() => platformBrowserDynamic().bootstrapModule(AppModule)) .catch(e => window.location.reload());
Closing thoughts for Part 5
In this part of the blog series we’ve coded the Keycloak Service and Keycloak HTTP classes, then we went over the environmental variables concept, updated the App module, and finally updated the Main.ts
class. We are now ready to make HTTP requests via the KeycloakHTTP
class which will automatically append the necessary headers in the request. We’ll do that in the next part of the series.
If you want more information on Keycloak, Angular, Bootstrap or Node JS you can visit the links below to read up, practice and improve your skillset.
- Angular: https://angular.io/docs
- Angular CLI Wiki: https://github.com/angular/angular-cli/wiki
- Bootstrap 4 Documentation: https://v4-alpha.getbootstrap.com/
- Node JS: https://nodejs.org/en/docs/
- Keycloak Server Administration Guide: https://www.gitbook.com/book/keycloak/server-adminstration-guide/details
- Keycloak Official Documentation: https://www.gitbook.com/book/keycloak/documentation/details
- Keycloak: http://www.keycloak.org/index.html
If you have any ideas or requests for implementing any Angular 2+ features (in the context of this blog series), feel free to share it with us in the comment section below.