Angular Keycloak: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(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()
        config: {
        .pipe(
          url: 'http://localhost:8080' + '/auth',
          switchMap<any, any>((config) => {
          realm: 'test',
 
          clientId: 'frontend',
            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" highlight="15,20-25">
<syntaxhighlight lang="ts">
@NgModule({
@NgModule({
   declarations: [
   declarations: [...],
    AppComponent,
   imports: [...],
    ContentComponent
  ],
   imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatFormFieldModule,
    MatSelectModule,
    MatTableModule,
    MatSnackBarModule,
    HttpClientModule,
    KeycloakAngularModule,
    NgbModule,
    AppRoutingModule
  ],
   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

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

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"
}