feat: add keycloak guard
MODIFY
package-lock.json
package-lock.json
}
}
},
+ "js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
}
}
},
+ "keycloak-angular": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-8.4.0.tgz",
+ "integrity": "sha512-3zBKzPf+ZG+16EfOdp1uxa/XTGElTOQSq5UkfFJzFcJe1D6p696hKIUJHNJECcYNUtt4XhYwUEolMIR+VySqgA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "keycloak-js": {
+ "version": "15.0.2",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-15.0.2.tgz",
+ "integrity": "sha512-dv2a4NcPSH3AzGWG3ZtB+VrHpuQLdFBYXtQBj/+oBzm6XNwnVAMdL6LIC0OzCLQpn3rKTQJtNSATAGhbKJgewQ==",
+ "requires": {
+ "base64-js": "1.3.1",
+ "js-sha256": "0.9.0"
+ },
+ "dependencies": {
+ "base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+ }
+ }
+ },
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",

MODIFY
package.json
package.json
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"axios": "^0.21.1",
+ "keycloak-angular": "^8.4.0",
+ "keycloak-js": "^15.0.2",
"ngx-markdown": "^12.0.1",
"rxjs": "~6.6.0",
"tslib": "^2.3.0",

MODIFY
src/app/app-routing.module.ts
src/app/app-routing.module.ts
@@ -6,6 +6,7 @@ import {MarketplaceSignupStepperComponent} from "./components/marketplace-signup
import {MarketplaceComponent} from "./components/marketplace/marketplace.component";
import {AccountComponent} from './components/account/account.component';
import {MyAppsComponent} from './components/my-apps/my-apps.component';
+import {AuthGuard} from "./guards/auth.guard";
const routes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
@@ -13,7 +14,7 @@ const routes: Routes = [
{path: 'marketplace', component: MarketplaceComponent},
{path: 'api-details/:orgId/:apiId', component: MarketplaceApiDetailsComponent},
{path: 'api-signup/:orgId/:apiId', component: MarketplaceSignupStepperComponent},
- {path: 'account', component: AccountComponent},
+ {path: 'account', component: AccountComponent, canActivate: [AuthGuard]},
{path: 'applications', component: MyAppsComponent},
];

MODIFY
src/app/app.module.ts
src/app/app.module.ts
@@ -30,12 +30,14 @@ import { PlanCardListComponent } from './components/plan-card-list/plan-card-lis
import {AccountComponent} from './components/account/account.component';
import {MyAppsComponent} from './components/my-apps/my-apps.component';
import {ThemeService} from './services/theme/theme.service';
+import {KeycloakAngularModule} from "keycloak-angular";
+import {KeycloakHelperService} from "./services/keycloak-helper/keycloak-helper.service";
export function initializeApp(configService: ConfigService,
devPortalInitializer: InitializerService): () => Promise<void> {
configService.readAndEvaluateConfig();
- /* Define promisses needed for the app initialization */
+ /* Define promises needed for the app initialization */
const initLanguagePromise: Promise<void> = new Promise((resolve, reject) => {
devPortalInitializer.initLanguage(configService.getLanguage()).then(() => {
resolve();
@@ -46,7 +48,7 @@ export function initializeApp(configService: ConfigService,
return (): Promise<void> => {
return new Promise((resolve, reject) => {
- /* Insert defined promisses */
+ /* Insert defined promises */
Promise.all([initLanguagePromise]).then(() => {
resolve();
}).catch((e) => {
@@ -60,6 +62,10 @@ export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './../assets/i18n/', '.json');
}
+function initializeKeycloak(keycloakHelper: KeycloakHelperService) {
+ return async () => await keycloakHelper.initKeycloak();
+}
+
@NgModule({
declarations: [
AppComponent,
@@ -82,6 +88,7 @@ export function createTranslateLoader(http: HttpClient) {
MyAppsComponent
],
imports: [
+ KeycloakAngularModule,
MaterialModule,
BrowserModule,
AppRoutingModule,
@@ -104,6 +111,12 @@ export function createTranslateLoader(http: HttpClient) {
useFactory: initializeApp,
multi: true,
deps: [ConfigService, InitializerService, ThemeService]
+ },
+ {
+ provide: APP_INITIALIZER,
+ useFactory: initializeKeycloak,
+ multi: true,
+ deps: [KeycloakHelperService]
}
],
bootstrap: [AppComponent]

MODIFY
src/app/components/account/account.component.html
src/app/components/account/account.component.html
<p>account works!</p>
+
+<ng-container *ngIf="userProfile">
+ <h2>User information</h2>
+ <table>
+ <tr *ngIf="userProfile.username">
+ <th scope="row">Username</th>
+ <td>{{ userProfile.username }}</td>
+ </tr>
+ <tr *ngIf="userProfile.firstName">
+ <th scope="row">First name</th>
+ <td>{{ userProfile.firstName }}</td>
+ </tr>
+ <tr *ngIf="userProfile.lastName">
+ <th scope="row">First name</th>
+ <td>{{ userProfile.lastName }}</td>
+ </tr>
+ <tr *ngIf="userProfile.email">
+ <th scope="row">E-mail</th>
+ <td>{{ userProfile.email }}</td>
+ </tr>
+ <tr>
+ <th scope="row">E-mail verified</th>
+ <td>{{ userProfile.emailVerified ? 'Yes' : 'No' }}</td>
+ </tr>
+ </table>
+</ng-container>
+
+<button mat-raised-button (click)="logout()" >Log-Out</button>

MODIFY
src/app/components/account/account.component.ts
src/app/components/account/account.component.ts
import { Component, OnInit } from '@angular/core';
import {HeroService} from '../../services/hero/hero.service';
import {TranslateService} from '@ngx-translate/core';
+import {KeycloakHelperService} from "../../services/keycloak-helper/keycloak-helper.service";
+import {KeycloakProfile} from "keycloak-js";
@Component({
selector: 'app-account',
@@ -9,11 +11,15 @@ import {TranslateService} from '@ngx-translate/core';
})
export class AccountComponent implements OnInit {
+ public userProfile: KeycloakProfile | null = null;
+
constructor(private heroService: HeroService,
- private translator: TranslateService) { }
+ private translator: TranslateService,
+ private keycloakHelper: KeycloakHelperService) { }
- ngOnInit(): void {
+ async ngOnInit() {
this.setUpHero();
+ this.userProfile = await this.keycloakHelper.getUserProfile();
}
private setUpHero(){
@@ -22,4 +28,8 @@ export class AccountComponent implements OnInit {
subtitle: this.translator.instant('ACCOUNT.SUBTITLE')
});
}
+
+ public logout(){
+ this.keycloakHelper.logout();
+ }
}

ADD
src/app/guards/auth.guard.spec.ts
+import { TestBed } from '@angular/core/testing';
+
+import { AuthGuard } from './auth.guard';
+
+describe('AuthGuard', () => {
+ let guard: AuthGuard;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ guard = TestBed.inject(AuthGuard);
+ });
+
+ it('should be created', () => {
+ expect(guard).toBeTruthy();
+ });
+});

ADD
src/app/guards/auth.guard.ts
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
+import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular";
+import {KeycloakHelperService} from "../services/keycloak-helper/keycloak-helper.service";
+
+@Injectable({
+ providedIn: 'root'
+})
+
+export class AuthGuard extends KeycloakAuthGuard {
+
+ constructor(protected readonly router: Router,
+ protected readonly keycloakAngular: KeycloakService,
+ private keycloakHelper: KeycloakHelperService) {
+ super(router, keycloakAngular);
+ }
+
+ /**
+ * If not authenticated this guard forces a login and starts the automatic token refresh mechanism
+ * @param route the current route
+ * @param state the current state
+ */
+ async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
+ if (!this.authenticated) {
+ console.log('Login required');
+ await this.keycloakAngular.login({
+ redirectUri: window.location.origin + state.url
+ });
+ } else {
+ // we are logged in and can set the tokens
+ this.keycloakHelper.setAndUpdateTokens();
+ }
+ return Promise.resolve(this.authenticated);
+ }
+}

MODIFY
src/app/interfaces/IConfig.ts
src/app/interfaces/IConfig.ts
@@ -4,7 +4,8 @@ export interface IConfig {
"endpoint": string;
"hero": IHero,
"navigation": INavigation,
- "footer": IFooter
+ "footer": IFooter,
+ "auth": IAuthProvider
}
export interface IHero {
@@ -35,4 +36,9 @@ export interface ILink{
"openInNewTab": boolean
}
+export interface IAuthProvider {
+ "url": string,
+ "realm": string,
+ "clientId": string
+}

MODIFY
src/app/services/config/config.service.ts
src/app/services/config/config.service.ts
import {Injectable} from '@angular/core';
import config from './../../../../config.json';
-import {IConfig, IFooter, IHero, INavigation} from '../../interfaces/IConfig';
+import {IAuthProvider, IConfig, IFooter, IHero, INavigation} from '../../interfaces/IConfig';
@Injectable({
providedIn: 'root'
@@ -8,7 +8,8 @@ import {IConfig, IFooter, IHero, INavigation} from '../../interfaces/IConfig';
export class ConfigService {
config!: IConfig;
- constructor() { }
+ constructor() {
+ }
readAndEvaluateConfig(): boolean {
this.config = config;
@@ -18,7 +19,6 @@ export class ConfigService {
} catch (e) {
throw Error('Invalid Config File');
}
-
return true;
}
@@ -41,4 +41,8 @@ export class ConfigService {
getEndpoint(): string {
return this.config.endpoint;
}
+
+ getAuth(): IAuthProvider {
+ return {...this.config.auth};
+ }
}

ADD
src/app/services/keycloak-helper/keycloak-helper.service.spec.ts
+import { TestBed } from '@angular/core/testing';
+
+import { KeycloakHelperService } from './keycloak-helper.service';
+
+describe('KeycloakHelperService', () => {
+ let service: KeycloakHelperService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(KeycloakHelperService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});

ADD
src/app/services/keycloak-helper/keycloak-helper.service.ts
+import {Injectable} from '@angular/core';
+import {KeycloakService} from "keycloak-angular";
+import {KeycloakInstance} from "keycloak-js";
+import {ConfigService} from "../config/config.service";
+
+@Injectable({
+ providedIn: 'root',
+ deps: [KeycloakService]
+})
+export class KeycloakHelperService {
+ private isLoggedIn = false;
+ private executed = false;
+
+ constructor(private readonly keycloak: KeycloakService, private configService: ConfigService) {
+ }
+
+ /**
+ * Init keycloak setting, this is called via APP Initializer
+ */
+ public async initKeycloak(): Promise<any> {
+ return this.keycloak.init({
+ config: {
+ url: this.configService.getAuth().url,
+ realm: this.configService.getAuth().realm,
+ clientId: this.configService.getAuth().clientId
+ },
+ initOptions: {
+ onLoad: 'check-sso',
+ checkLoginIframe: false,
+ token: window.sessionStorage.getItem('KEYCLOAK_SESSION_STORAGE_TOKEN')!,
+ refreshToken: window.sessionStorage.getItem('KEYCLOAK_SESSION_STORAGE_REFRESH_TOKEN')!,
+ },
+ loadUserProfileAtStartUp: true, // because of https://github.com/mauriciovigolo/keycloak-angular/pull/269
+ enableBearerInterceptor: true,
+ bearerExcludedUrls: ['/assets', '/clients/public'] //TODO
+ }).then(response => {
+ console.log(response);
+ })
+ }
+
+ public async getUserProfile() {
+ this.isLoggedIn = await this.keycloak.isLoggedIn();
+ if (this.isLoggedIn) {
+ return await this.keycloak.loadUserProfile();
+ }
+ return null;
+ }
+
+ public logout(): void {
+ // TODO bring you home
+ KeycloakHelperService.clearTokensFromSessionStorage();
+ this.keycloak.logout('http://localhost:4200/home');
+ }
+
+ /**
+ * Will set the current tokens after login, and start an automatic refresh of tokens.
+ * Could be triggered multiple times but should only execute once
+ */
+ public setAndUpdateTokens() {
+ if (!this.executed) {
+ KeycloakHelperService.setTokensToSessionStorage(this.keycloak.getKeycloakInstance());
+ setInterval(() => {
+ this.keycloak.updateToken().then(() => {
+ console.log("Token successfully refreshed", new Date());
+ KeycloakHelperService.setTokensToSessionStorage(this.keycloak.getKeycloakInstance());
+ }).catch(() => {
+ console.log("Error refreshing token", new Date())
+ KeycloakHelperService.clearTokensFromSessionStorage();
+ });
+ }, Math.min((this.keycloak.getKeycloakInstance().tokenParsed!.exp! - 60) * 1000, 4 * 60 * 1000)); // refresh token minimum every 4 minutes)
+ }
+ this.executed = true;
+ }
+
+ private static setTokensToSessionStorage(keycloakInstance: KeycloakInstance) {
+ console.log('Set token:' + keycloakInstance.token);
+ window.sessionStorage.setItem('KEYCLOAK_SESSION_STORAGE_TOKEN', keycloakInstance.token!);
+ window.sessionStorage.setItem('KEYCLOAK_SESSION_STORAGE_REFRESH_TOKEN', keycloakInstance.refreshToken!);
+ }
+
+ private static clearTokensFromSessionStorage() {
+ window.sessionStorage.removeItem('KEYCLOAK_SESSION_STORAGE_TOKEN');
+ window.sessionStorage.removeItem('KEYCLOAK_SESSION_STORAGE_REFRESH_TOKEN');
+ }
+}

feat: add keycloak guard