Angular Keycloak: Difference between revisions
(One intermediate revision by the same user not shown) | |||
Line 10: | Line 10: | ||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
npm install keycloak-angular keycloak-js | npm install keycloak-angular keycloak-js | ||
</syntaxhighlight> | |||
==Config Service== | |||
This reads the appropriate configuration | |||
<syntaxhighlight lang="ts"> | |||
import { environment } from '../../environments/environment'; | |||
@Injectable({ | |||
providedIn: 'root' | |||
}) | |||
export class ConfigInitService { | |||
private config: any; | |||
constructor(private httpClient: HttpClient) {} | |||
public getConfig(): Observable<any> { | |||
return this.httpClient | |||
.get(this.getConfigFile(), { | |||
observe: 'response', | |||
}) | |||
.pipe( | |||
catchError((error) => { | |||
console.log(error) | |||
return of(null) | |||
} ), | |||
mergeMap((response) => { | |||
if (response && response.body) { | |||
this.config = response.body; | |||
return of(this.config); | |||
} else { | |||
return of(null); | |||
} | |||
})); | |||
} | |||
private getConfigFile(): string { | |||
return environment.configFile | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create Initializer Factory== | ==Create Initializer Factory== | ||
Line 104: | Line 144: | ||
*Content-Security-Policy frame-src 'self'; frame-ancestors 'self' http://127.0.0.1 http://localhost:8080/ object-src 'none'; | *Content-Security-Policy frame-src 'self'; frame-ancestors 'self' http://127.0.0.1 http://localhost:8080/ object-src 'none'; | ||
These needed to be modified to work. To fix you are the host ip e.g. http://localhost:4200 | These needed to be modified to work. To fix you are the host ip e.g. http://localhost:4200 | ||
=Setup= | =Prod and Development Setup= | ||
==Configuration Setup== | ==Configuration Setup== | ||
We create a dev and production environment. Under assets/config | We create a dev and production environment. Under assets/config | ||
Line 199: | Line 239: | ||
}, | }, | ||
.... | .... | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 12:37, 18 April 2021
Introduction
This is a page just to clarify how to integrate Keycloak with Angular. It is assumed you know how to configure Keycloak. Most of this is from ["Wojciech Krzywiec"]
Keycloak
So to set this up we
- Setup a client with
- A root URL http://localhost:80/*, http://localhost:4200/*, http://localhost/*
- Web Origins +
Application
Install the Keycloak Package
npm install keycloak-angular keycloak-js
Config Service
This reads the appropriate configuration
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ConfigInitService {
private config: any;
constructor(private httpClient: HttpClient) {}
public getConfig(): Observable<any> {
return this.httpClient
.get(this.getConfigFile(), {
observe: 'response',
})
.pipe(
catchError((error) => {
console.log(error)
return of(null)
} ),
mergeMap((response) => {
if (response && response.body) {
this.config = response.body;
return of(this.config);
} else {
return of(null);
}
}));
}
private getConfigFile(): string {
return environment.configFile
}
}
Create Initializer Factory
Never keen on the CLI but I guess it stays up to date.
ng g class init/keycloak-init --type=factory --skip-tests
This provides the initializer for Keycloak Server
import { KeycloakService } from "keycloak-angular";
export function initializeKeycloak(
keycloak: KeycloakService,
configService: ConfigInitService
) {
return () =>
configService.getConfig()
.pipe(
switchMap<any, any>((config) => {
return fromPromise(keycloak.init({
config: {
url: config['KEYCLOAK_URL'] + '/auth',
realm: config['KEYCLOAK_REALM'],
clientId: config['KEYCLOAK_CLIENT_ID'],
}
}))
})
).toPromise()
}
Now add it to the app module
@NgModule({
declarations: [...],
imports: [...],
providers: [
ConfigInitService,
{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService, ConfigInitService],
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Create a Guard
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Promise<boolean | UrlTree> {
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
}
return this.authenticated;
}
}
Add the Guard to the Route
const routes: Routes = [
{ path: '', component: ContentComponent , canActivate: [AuthGuard]},
{ path: '**', redirectTo: '' }
];
CSP and Keycloak
The default installation I used had the Realm Settings->Security Defences with
- X-Frame-Options SAMEORIGIN
- Content-Security-Policy frame-src 'self'; frame-ancestors 'self' http://127.0.0.1 http://localhost:8080/ object-src 'none';
These needed to be modified to work. To fix you are the host ip e.g. http://localhost:4200
Prod and Development Setup
Configuration Setup
We create a dev and production environment. Under assets/config
Dev config.dev.json
{
"KEYCLOAK_URL": "http://localhost:8080",
"KEYCLOAK_REALM": "test",
"KEYCLOAK_CLIENT_ID": "frontend"
}
Prod config.prod.json
{
"KEYCLOAK_URL": "${KEYCLOAK_URL}",
"KEYCLOAK_REALM": "${KEYCLOAK_REALM}",
"KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}"
}
Environment Setup
Under Environments create a default (copy of dev) dev and production configuration
Prod environment.prod.ts
export const environment = {
production: true,
configFile: 'assets/config/config.prod.json'
};
Dev environment.dev.ts
export const environment = {
production: false,
configFile: 'assets/config/config.dev.json'
};
Angular Setup
So need to let angular know their are two environments
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
...
"configurations": {
"dev": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dev.ts"
}
]
},
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
...
In the same file, scroll down a little bit to the serve section and in configurations add new dev entry with browserTarget. Replace project name.
"architect": {
"build": {...},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {...},
"configurations": {
"dev": {
"browserTarget": "<prodject_name>:build:dev"
},
"production": {
"browserTarget": "<prodject_name>:build:production"
}
}
},
Now we can pass the configuration in the command line
{
"name": "testproject",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config src/assets/proxy.conf.dev.json -c dev",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"postinstall": "ngcc"
},
....
}
Useful Bash Command
Here is a way to read in a file and substitute the values
#!/bin/bash
envsubst < /usr/share/nginx/html/assets/config/config.prod.json > /usr/share/nginx/html/assets/config/config.json
envsubst "\$BACKEND_BASE_PATH" < /temp/default.conf > /etc/nginx/conf.d/default.conf
exec "$@"
So here we go given the file
{
"KEYCLOAK_URL": "${KEYCLOAK_URL}",
"KEYCLOAK_REALM": "${KEYCLOAK_REALM}",
"KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}"
}
And the environment
export KEYCLOAK_URL=http://localhost:8080
export KEYCLOAK_REALM=test
export KEYCLOAK_CLIENT_ID=ncc-1701
We run
envsubst < test.json >test_out.json
And we get
{
"KEYCLOAK_URL": "http://localhost:8080",
"KEYCLOAK_REALM": "test",
"KEYCLOAK_CLIENT_ID": "ncc-1701"
}