Zum Inhalt springen

Angular Integration

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

This guide covers how to integrate Skycloak authentication into Angular applications using the Keycloak Angular adapter and Angular’s built-in security features.

  • Angular 17+ application (recommended), or Angular 15-16 for legacy setup
  • Node.js 18+
  • Skycloak cluster with configured realm and client
  • Basic understanding of Angular services and guards

This is the recommended approach for Angular 17+ applications using standalone components.

npm install keycloak-angular keycloak-js

For Angular 17+ with standalone configuration:

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideKeycloak, withAutoRefreshToken, AutoRefreshTokenService, UserActivityService, INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG } from 'keycloak-angular';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptorsFromDi()),
    provideKeycloak({
      config: {
        url: 'https://your-cluster-id.app.skycloak.io',
        realm: 'your-realm',
        clientId: 'your-angular-app',
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
        checkLoginIframe: false,
      },
      features: [
        withAutoRefreshToken({
          onInactivityTimeout: 'logout',
          sessionTimeout: 60000,
        }),
      ],
      providers: [AutoRefreshTokenService, UserActivityService],
    }),
    {
      provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
      useValue: {
        allowedList: [/\/api\/.*/],
        excludedList: ['/assets', '/public'],
      },
    },
  ],
});
// src/app/guards/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, ActivatedRouteSnapshot } from '@angular/router';
import Keycloak from 'keycloak-js';

export const authGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
  const keycloak = inject(Keycloak);
  const router = inject(Router);

  const isAuthenticated = keycloak.authenticated ?? false;

  if (!isAuthenticated) {
    await keycloak.login({
      redirectUri: window.location.origin + '/' + route.url.join('/'),
    });
    return false;
  }

  // Check for required roles
  const requiredRoles = route.data['roles'] as string[] | undefined;
  if (!requiredRoles || requiredRoles.length === 0) {
    return true;
  }

  const userRoles = keycloak.realmAccess?.roles ?? [];
  const hasRequiredRoles = requiredRoles.every(role => userRoles.includes(role));

  if (!hasRequiredRoles) {
    return router.createUrlTree(['/access-denied']);
  }

  return true;
};
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) },
  { path: 'public', loadComponent: () => import('./pages/public/public.component').then(m => m.PublicComponent) },
  {
    path: 'profile',
    loadComponent: () => import('./pages/profile/profile.component').then(m => m.ProfileComponent),
    canActivate: [authGuard],
  },
  {
    path: 'admin',
    loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent),
    canActivate: [authGuard],
    data: { roles: ['admin'] },
  },
  { path: '**', redirectTo: '' },
];

Legacy Setup (Angular 15-16 with NgModules)

Section titled “Legacy Setup (Angular 15-16 with NgModules)”

For applications using the legacy module-based approach with Angular 15-16 or keycloak-angular v18.x.

npm install keycloak-angular@18 keycloak-js

Create a Keycloak initialization function:

// src/app/init/keycloak-init.factory.ts
import { KeycloakService } from 'keycloak-angular';

export function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        url: 'https://your-cluster-id.app.skycloak.io',
        realm: 'your-realm',
        clientId: 'your-angular-app',
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri:
          window.location.origin + '/assets/silent-check-sso.html',
        checkLoginIframe: false,
      },
      enableBearerInterceptor: true,
      bearerPrefix: 'Bearer',
      bearerExcludedUrls: ['/assets', '/public'],
    });
}
// src/app/app.module.ts
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { initializeKeycloak } from './init/keycloak-init.factory';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    KeycloakAngularModule,
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService],
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard extends KeycloakAuthGuard {
  constructor(
    protected override readonly router: Router,
    protected readonly keycloak: KeycloakService
  ) {
    super(router, keycloak);
  }

  async isAccessAllowed(
    route: ActivatedRouteSnapshot
  ): Promise<boolean | UrlTree> {
    // Force login if not authenticated
    if (!this.authenticated) {
      await this.keycloak.login({
        redirectUri: window.location.origin + route.url.join('/'),
      });
      return false;
    }

    // Check for required roles
    const requiredRoles = route.data['roles'];
    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

    // Check if user has required roles
    const hasRequiredRoles = requiredRoles.every((role: string) =>
      this.roles.includes(role)
    );

    if (!hasRequiredRoles) {
      // Navigate to access denied page
      return this.router.createUrlTree(['/access-denied']);
    }

    return true;
  }
}
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { HomeComponent } from './components/home/home.component';
import { AdminComponent } from './components/admin/admin.component';
import { ProfileComponent } from './components/profile/profile.component';
import { PublicComponent } from './components/public/public.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'public', component: PublicComponent },
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [AuthGuard],
  },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    data: { roles: ['admin'] },
  },
  { path: '**', redirectTo: '' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile, KeycloakTokenParsed } from 'keycloak-js';
import { Observable, from, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

export interface UserInfo {
  id: string;
  username: string;
  email: string;
  firstName: string;
  lastName: string;
  roles: string[];
  groups: string[];
  attributes: { [key: string]: any };
}

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(private keycloakService: KeycloakService) {}

  public getLoggedUser(): Observable<UserInfo | null> {
    if (!this.keycloakService.isLoggedIn()) {
      return of(null);
    }

    return from(this.keycloakService.loadUserProfile()).pipe(
      map((profile: KeycloakProfile) => {
        const token = this.keycloakService.getKeycloakInstance().tokenParsed as KeycloakTokenParsed;
        
        return {
          id: profile.id || token.sub || '',
          username: profile.username || '',
          email: profile.email || '',
          firstName: profile.firstName || '',
          lastName: profile.lastName || '',
          roles: this.keycloakService.getUserRoles(true),
          groups: token['groups'] || [],
          attributes: profile.attributes || {},
        };
      }),
      catchError(() => of(null))
    );
  }

  public login(redirectUri?: string): void {
    this.keycloakService.login({ redirectUri });
  }

  public logout(redirectUri?: string): void {
    this.keycloakService.logout(redirectUri);
  }

  public register(redirectUri?: string): void {
    this.keycloakService.register({ redirectUri });
  }

  public async getToken(): Promise<string> {
    try {
      await this.keycloakService.updateToken(30);
      return await this.keycloakService.getToken();
    } catch (error) {
      console.error('Failed to refresh token', error);
      throw error;
    }
  }

  public hasRole(role: string): boolean {
    return this.keycloakService.isUserInRole(role);
  }

  public hasRealmRole(role: string): boolean {
    return this.keycloakService.isUserInRole(role, 'realm');
  }

  public hasResourceRole(role: string, resource?: string): boolean {
    return this.keycloakService.isUserInRole(role, resource);
  }

  public hasAnyRole(roles: string[]): boolean {
    return roles.some(role => this.hasRole(role));
  }

  public hasAllRoles(roles: string[]): boolean {
    return roles.every(role => this.hasRole(role));
  }
}
// src/app/components/profile/profile.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService, UserInfo } from '../../services/auth.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-profile',
  template: `
    <div class="profile-container" *ngIf="userInfo$ | async as user">
      <h2>User Profile</h2>
      <div class="profile-info">
        <p><strong>Username:</strong> {{ user.username }}</p>
        <p><strong>Email:</strong> {{ user.email }}</p>
        <p><strong>Name:</strong> {{ user.firstName }} {{ user.lastName }}</p>
        
        <div class="roles">
          <h3>Roles</h3>
          <ul>
            <li *ngFor="let role of user.roles">{{ role }}</li>
          </ul>
        </div>
        
        <div class="groups" *ngIf="user.groups.length > 0">
          <h3>Groups</h3>
          <ul>
            <li *ngFor="let group of user.groups">{{ group }}</li>
          </ul>
        </div>
      </div>
      
      <button (click)="logout()" class="logout-btn">Logout</button>
    </div>
  `,
})
export class ProfileComponent implements OnInit {
  userInfo$!: Observable<UserInfo | null>;

  constructor(private authService: AuthService) {}

  ngOnInit(): void {
    this.userInfo$ = this.authService.getLoggedUser();
  }

  logout(): void {
    this.authService.logout();
  }
}
// src/app/interceptors/token.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse,
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap, finalize } from 'rxjs/operators';
import { KeycloakService } from 'keycloak-angular';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private keycloakService: KeycloakService) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    // Skip interceptor for excluded URLs
    if (this.isExcludedUrl(request.url)) {
      return next.handle(request);
    }

    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          return this.handle401Error(request, next);
        }
        return throwError(() => error);
      })
    );
  }

  private handle401Error(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return from(this.keycloakService.updateToken(30)).pipe(
        switchMap((refreshed: boolean) => {
          if (refreshed) {
            this.refreshTokenSubject.next(refreshed);
            return next.handle(request);
          }
          // Refresh failed, redirect to login
          this.keycloakService.login();
          return throwError(() => new Error('Token refresh failed'));
        }),
        catchError((err) => {
          this.keycloakService.login();
          return throwError(() => err);
        }),
        finalize(() => {
          this.isRefreshing = false;
        })
      );
    }

    return this.refreshTokenSubject.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap(() => next.handle(request))
    );
  }

  private isExcludedUrl(url: string): boolean {
    const excludedUrls = ['/assets', '/public', 'i18n'];
    return excludedUrls.some((excluded) => url.includes(excluded));
  }
}
// src/app/directives/has-role.directive.ts
import {
  Directive,
  Input,
  OnInit,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import { AuthService } from '../services/auth.service';

@Directive({
  selector: '[appHasRole]',
})
export class HasRoleDirective implements OnInit {
  @Input() appHasRole!: string | string[];
  @Input() appHasRoleElse?: TemplateRef<any>;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) {}

  ngOnInit(): void {
    const roles = Array.isArray(this.appHasRole)
      ? this.appHasRole
      : [this.appHasRole];

    if (this.authService.hasAnyRole(roles)) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else if (this.appHasRoleElse) {
      this.viewContainer.createEmbeddedView(this.appHasRoleElse);
    } else {
      this.viewContainer.clear();
    }
  }
}

Usage in templates:

<!-- Show only for admins -->
<div *appHasRole="'admin'">
  <button (click)="deleteUser()">Delete User</button>
</div>

<!-- Show for multiple roles -->
<div *appHasRole="['admin', 'moderator']">
  <button (click)="moderateContent()">Moderate</button>
</div>

<!-- With else template -->
<div *appHasRole="'premium-user'; else standardUser">
  <app-premium-features></app-premium-features>
</div>
<ng-template #standardUser>
  <app-standard-features></app-standard-features>
</ng-template>
// src/app/services/permission.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, shareReplay } from 'rxjs/operators';

interface Permission {
  resource: string;
  scopes: string[];
}

@Injectable({
  providedIn: 'root',
})
export class PermissionService {
  private permissions$?: Observable<Permission[]>;

  constructor(private http: HttpClient) {}

  loadPermissions(): Observable<Permission[]> {
    if (!this.permissions$) {
      this.permissions$ = this.http.get<Permission[]>('/api/permissions').pipe(
        shareReplay(1),
        catchError(() => of([]))
      );
    }
    return this.permissions$;
  }

  hasPermission(resource: string, scope: string): Observable<boolean> {
    return this.loadPermissions().pipe(
      map(permissions => {
        const permission = permissions.find(p => p.resource === resource);
        return permission ? permission.scopes.includes(scope) : false;
      })
    );
  }

  canAccess(resource: string): Observable<boolean> {
    return this.hasPermission(resource, 'read');
  }

  canEdit(resource: string): Observable<boolean> {
    return this.hasPermission(resource, 'write');
  }

  canDelete(resource: string): Observable<boolean> {
    return this.hasPermission(resource, 'delete');
  }
}

Create src/assets/silent-check-sso.html:

<!DOCTYPE html>
<html>
<head>
  <title>Silent SSO Check</title>
</head>
<body>
  <script>
    parent.postMessage(location.href, location.origin);
  </script>
</body>
</html>
// src/app/services/token-refresh.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class TokenRefreshService implements OnDestroy {
  private destroy$ = new Subject<void>();
  private refreshInterval = 4 * 60 * 1000; // 4 minutes

  constructor(private keycloakService: KeycloakService) {}

  startTokenRefresh(): void {
    interval(this.refreshInterval)
      .pipe(takeUntil(this.destroy$))
      .subscribe(async () => {
        try {
          const refreshed = await this.keycloakService.updateToken(300);
          if (refreshed) {
            console.log('Token refreshed successfully');
          }
        } catch (error) {
          console.error('Failed to refresh token:', error);
          await this.keycloakService.login();
        }
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
// src/app/testing/mock-keycloak.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class MockKeycloakService {
  private _isLoggedIn = true;
  private _roles = ['user', 'admin'];
  private _token = 'mock-token';

  init(): Promise<boolean> {
    return Promise.resolve(true);
  }

  isLoggedIn(): boolean {
    return this._isLoggedIn;
  }

  getUserRoles(allRoles?: boolean): string[] {
    return this._roles;
  }

  getToken(): Promise<string> {
    return Promise.resolve(this._token);
  }

  updateToken(minValidity: number): Promise<boolean> {
    return Promise.resolve(true);
  }

  login(options?: any): Promise<void> {
    return Promise.resolve();
  }

  logout(redirectUri?: string): Promise<void> {
    return Promise.resolve();
  }

  isUserInRole(role: string, resource?: string): boolean {
    return this._roles.includes(role);
  }

  loadUserProfile(): Promise<any> {
    return Promise.resolve({
      id: 'test-user-123',
      username: 'testuser',
      email: 'test@example.com',
      firstName: 'Test',
      lastName: 'User',
    });
  }

  // Test helpers
  setLoggedIn(loggedIn: boolean): void {
    this._isLoggedIn = loggedIn;
  }

  setRoles(roles: string[]): void {
    this._roles = roles;
  }
}
// src/app/components/profile/profile.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeycloakService } from 'keycloak-angular';
import { ProfileComponent } from './profile.component';
import { AuthService } from '../../services/auth.service';
import { MockKeycloakService } from '../../testing/mock-keycloak.service';

describe('ProfileComponent', () => {
  let component: ProfileComponent;
  let fixture: ComponentFixture<ProfileComponent>;
  let authService: AuthService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ProfileComponent],
      providers: [
        AuthService,
        { provide: KeycloakService, useClass: MockKeycloakService },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(ProfileComponent);
    component = fixture.componentInstance;
    authService = TestBed.inject(AuthService);
  });

  it('should display user information', async () => {
    fixture.detectChanges();
    await fixture.whenStable();

    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('.profile-info')).toBeTruthy();
    expect(compiled.textContent).toContain('testuser');
    expect(compiled.textContent).toContain('test@example.com');
  });

  it('should show roles', async () => {
    fixture.detectChanges();
    await fixture.whenStable();

    const roles = fixture.nativeElement.querySelectorAll('.roles li');
    expect(roles.length).toBe(2);
    expect(roles[0].textContent).toContain('user');
    expect(roles[1].textContent).toContain('admin');
  });
});
// src/environments/environment.prod.ts
export const environment = {
  production: true,
  keycloak: {
    url: 'https://auth.yourcompany.com',
    realm: 'production',
    clientId: 'angular-app',
  },
  apiUrl: 'https://api.yourcompany.com',
};

// src/environments/environment.ts
export const environment = {
  production: false,
  keycloak: {
    url: 'https://dev-auth.yourcompany.com',
    realm: 'development',
    clientId: 'angular-app-dev',
  },
  apiUrl: 'http://localhost:8080',
};
// src/app/services/error-handler.service.ts
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root',
})
export class ErrorHandlerService {
  constructor(
    private router: Router,
    private keycloakService: KeycloakService
  ) {}

  handleError(error: HttpErrorResponse): void {
    switch (error.status) {
      case 401:
        // Unauthorized - refresh token or redirect to login
        this.handleUnauthorized();
        break;
      case 403:
        // Forbidden - redirect to access denied
        this.router.navigate(['/access-denied']);
        break;
      case 404:
        // Not found
        this.router.navigate(['/not-found']);
        break;
      default:
        // Generic error
        console.error('An error occurred:', error);
    }
  }

  private async handleUnauthorized(): Promise<void> {
    try {
      const refreshed = await this.keycloakService.updateToken(30);
      if (!refreshed) {
        await this.keycloakService.login();
      }
    } catch {
      await this.keycloakService.login();
    }
  }
}
  1. Infinite Redirect Loop

    • Check redirect URI configuration in Skycloak
    • Verify onLoad option in init configuration
    • Ensure cookies are enabled
  2. Token Not Attached to Requests

    • Verify HTTP interceptor is provided
    • Check bearer excluded URLs configuration
    • Ensure interceptor order in providers
  3. Role Check Failures

    • Verify role mappings in Skycloak
    • Check realm vs client role configuration
    • Debug token claims in browser console
// src/app/utils/keycloak-debug.ts
export function debugKeycloak(keycloakService: KeycloakService): void {
  const keycloak = keycloakService.getKeycloakInstance();
  
  console.group('Keycloak Debug Info');
  console.log('Authenticated:', keycloak.authenticated);
  console.log('Subject:', keycloak.subject);
  console.log('Token:', keycloak.token);
  console.log('Parsed Token:', keycloak.tokenParsed);
  console.log('Refresh Token:', keycloak.refreshToken);
  console.log('Realm Roles:', keycloak.realmAccess?.roles);
  console.log('Resource Roles:', keycloak.resourceAccess);
  console.groupEnd();
}