Angular Keycloak: Difference between revisions
(14 intermediate revisions 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 21: | Line 61: | ||
export function initializeKeycloak( | export function initializeKeycloak( | ||
keycloak: KeycloakService | keycloak: KeycloakService, | ||
configService: ConfigInitService | |||
) { | ) { | ||
return () => | return () => | ||
keycloak.init({ | 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() | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Now add it to the app module | Now add it to the app module | ||
<syntaxhighlight lang="ts | <syntaxhighlight lang="ts"> | ||
@NgModule({ | @NgModule({ | ||
declarations: [ | declarations: [...], | ||
imports: [...], | |||
imports: [ | |||
providers: [ | providers: [ | ||
ConfigInitService, | |||
{ | { | ||
provide: APP_INITIALIZER, | provide: APP_INITIALIZER, | ||
useFactory: initializeKeycloak, | useFactory: initializeKeycloak, | ||
multi: true, | multi: true, | ||
deps: [KeycloakService], | deps: [KeycloakService, ConfigInitService], | ||
} | } | ||
], | ], | ||
Line 65: | Line 99: | ||
export class AppModule { } | export class AppModule { } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create a Guard== | ==Create a Guard== | ||
<syntaxhighlight lang="ts"> | <syntaxhighlight lang="ts"> | ||
Line 103: | Line 138: | ||
{ path: '**', redirectTo: '' } | { path: '**', redirectTo: '' } | ||
]; | ]; | ||
</syntaxhighlight> | |||
==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=== | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"KEYCLOAK_URL": "http://localhost:8080", | |||
"KEYCLOAK_REALM": "test", | |||
"KEYCLOAK_CLIENT_ID": "frontend" | |||
} | |||
</syntaxhighlight> | |||
===Prod config.prod.json=== | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"KEYCLOAK_URL": "${KEYCLOAK_URL}", | |||
"KEYCLOAK_REALM": "${KEYCLOAK_REALM}", | |||
"KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}" | |||
} | |||
</syntaxhighlight> | |||
==Environment Setup== | |||
Under Environments create a default (copy of dev) dev and production configuration | |||
===Prod environment.prod.ts=== | |||
<syntaxhighlight lang="json"> | |||
export const environment = { | |||
production: true, | |||
configFile: 'assets/config/config.prod.json' | |||
}; | |||
</syntaxhighlight> | |||
===Dev environment.dev.ts=== | |||
<syntaxhighlight lang="json"> | |||
export const environment = { | |||
production: false, | |||
configFile: 'assets/config/config.dev.json' | |||
}; | |||
</syntaxhighlight> | |||
==Angular Setup== | |||
So need to let angular know their are two environments | |||
<syntaxhighlight lang="json"> | |||
"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", | |||
... | |||
</syntaxhighlight> | |||
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. | |||
<syntaxhighlight lang="json"> | |||
"architect": { | |||
"build": {...}, | |||
"serve": { | |||
"builder": "@angular-devkit/build-angular:dev-server", | |||
"options": {...}, | |||
"configurations": { | |||
"dev": { | |||
"browserTarget": "<prodject_name>:build:dev" | |||
}, | |||
"production": { | |||
"browserTarget": "<prodject_name>:build:production" | |||
} | |||
} | |||
}, | |||
</syntaxhighlight> | |||
Now we can pass the configuration in the command line | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"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" | |||
}, | |||
.... | |||
} | |||
</syntaxhighlight> | |||
==Useful Bash Command== | |||
Here is a way to read in a file and substitute the values | |||
<syntaxhighlight lang="bash"> | |||
#!/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 "$@" | |||
</syntaxhighlight> | |||
So here we go given the file | |||
<syntaxhighlight lang="bash"> | |||
{ | |||
"KEYCLOAK_URL": "${KEYCLOAK_URL}", | |||
"KEYCLOAK_REALM": "${KEYCLOAK_REALM}", | |||
"KEYCLOAK_CLIENT_ID": "${KEYCLOAK_CLIENT_ID}" | |||
} | |||
</syntaxhighlight> | |||
And the environment | |||
<syntaxhighlight lang="bash"> | |||
export KEYCLOAK_URL=http://localhost:8080 | |||
export KEYCLOAK_REALM=test | |||
export KEYCLOAK_CLIENT_ID=ncc-1701 | |||
</syntaxhighlight> | |||
We run | |||
<syntaxhighlight lang="bash"> | |||
envsubst < test.json >test_out.json | |||
</syntaxhighlight> | |||
And we get | |||
<syntaxhighlight lang="bash"> | |||
{ | |||
"KEYCLOAK_URL": "http://localhost:8080", | |||
"KEYCLOAK_REALM": "test", | |||
"KEYCLOAK_CLIENT_ID": "ncc-1701" | |||
} | |||
</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"
}