Skip to content

Commit

Permalink
feat: improved documentation and examples
Browse files Browse the repository at this point in the history
- Add migration guide for v19
- Improved README.md
- Migrated examples to use inject()
  • Loading branch information
mauriciovigolo committed Dec 21, 2024
1 parent 7d76f62 commit 2664d0c
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 15 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@

## About

`Keycloak-Angular` is a library that makes it easier to use [Keycloak-js](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) in Angular applications. It provides the following features:
`Keycloak-Angular` is a library that makes it easier to use [keycloak-js](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) in Angular applications. It provides the following features:

- **Easy Initialization**: Use the `provideKeycloak` function to set up and initialize a Keycloak instance with `provideAppInitializer`. This simplifies the setup process.
- **Angular DI Support**: The Keycloak client instance can be injected directly into Angular components, services, and other parts of your app. There’s no need to create a custom service to wrap the client.
- **HTTP Interceptors**: Add the Bearer token to the `Authorization` header with built-in interceptors. These are modular and easy to configure using injection tokens. You can also create your own interceptors with provided helper functions.
- **Keycloak Events as Signals**: Access Keycloak events easily using an Angular Signal, provided through the `KEYCLOAK_EVENT_SIGNAL` injection token.
- **Automatic Token Refresh**: Use the `withRefreshToken` feature to automatically refresh tokens when they expire, considering the user activity and a sessionTimeout.
- **Keycloak Events as Signals**: Access Keycloak events easily using Angular Signals, provided through the `KEYCLOAK_EVENT_SIGNAL` injection token.
- **Automatic Token Refresh**: Use the `withAutoRefreshToken` feature to automatically refresh tokens when they expire, considering the user activity and a sessionTimeout.
- **Custom Route Guards**: Use the `createAuthGuard` factory to build guards for `CanActivate` and `CanActivateChild`, helping you secure routes with custom logic.
- **Role-Based Rendering**: The `*kaHasRoles` directive lets you show or hide parts of the DOM based on the user’s resource or realm roles.

Expand All @@ -46,7 +46,7 @@ Run the following command to install both Keycloak Angular and the official Keyc
npm install keycloak-angular keycloak-js
```

Note that `keycloak-js` is a peer dependency of Keycloak Angular. This allows greater flexibility of choosing the right version of the Keycloak client version for your project.
Note that `keycloak-js` is a peer dependency of `keycloak-angular`. This allows greater flexibility of choosing the right version of the Keycloak client for your project.

### Versions

Expand All @@ -67,7 +67,7 @@ The Keycloak client documentation recommends to use the same version of your Key

## Setup

To initialize Keycloak, add `provideKeycloak` to the `providers` array in the `ApplicationConfig` object (e.g., in `app.config.ts`). If `initOptions` is included, Keycloak-Angular will automatically use `provideAppInitializer` to handle initialization, following the recommended approach for starting dependencies.
To initialize Keycloak, add `provideKeycloak` to the `providers` array in the `ApplicationConfig` object (e.g., in `app.config.ts`). If `initOptions` is included, `keycloak-angular` will automatically use `provideAppInitializer` to handle initialization, following the recommended approach for starting dependencies.

Use the example code below as a guide for your application.

Expand Down Expand Up @@ -101,7 +101,7 @@ export const appConfig: ApplicationConfig = {
>
> 1. You do not need to call `provideAppInitializer` if `initOptions` is provided. The library handles this automatically.
> 2. If you need to control when `keycloak.init` is called, do not pass the `initOptions` to the `provideKeycloak` function. In this case, you are responsible for calling `keycloak.init` manually.
> 3. For reference, Angular `APP_INITIALIZER` is deprecated. The recommended approach is to use `provideAppInitializer`.
> 3. For reference, Angular's `APP_INITIALIZER` is deprecated. The recommended approach is to use `provideAppInitializer`.
In the example, Keycloak is configured to use a silent `check-sso`. With this feature enabled, your browser avoids a full redirect to the Keycloak server and back to your application. Instead, this action is performed in a hidden iframe, allowing your application resources to be loaded and parsed only once during initialization, rather than reloading after a redirect.

Expand Down
313 changes: 313 additions & 0 deletions docs/migration-guides/v19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
# Migration Guide: Keycloak Angular v19

Keycloak Angular v19 introduces significant changes and improvements that align with modern Angular practices and address longstanding issues.

In previous versions, wrapping the `keycloak-js` client introduced challenges such as maintaining outdated Promise mappings, tightly coupled configurations, and limited flexibility with interceptors and events. Additionally, the evolution of Angular towards a functional paradigm called for a comprehensive refactor.

Keycloak Angular v19 embraces Angular's functional design and introduces a range of enhancements:

- The new `provideKeycloak` function simplifies client initialization at the application startup, leveraging Angular's `provideAppInitializer`. See [Bootstrapping Keycloak with `provideKeycloak`](#bootstrapping-keycloak-with-providekeycloak).
- Direct usage of the `keycloak-js` client eliminates the need for a wrapper service. See [Migrating from `KeycloakService`](#migrating-from-keycloakservice).
- Refactored token interceptors now rely on injection tokens for configuration, promoting modularity and reducing complexity. See [Migrating from `KeycloakBearerInterceptor`](#migrating-from-keycloakbearerinterceptor).
- The legacy `KeycloakAuthGuard` has been replaced with higher-order functions for creating custom route guards. See [Migrating from `KeycloakAuthGuard`](#migrating-from-keycloakauthguard).
- Event handling has transitioned from RxJS Subjects to Angular Signals, improving clarity and integration. See [Migrating from `KeycloakEvents` RxJS Subject](#migrating-from-keycloakevents-rxjs-subject).

These changes enable better scalability, configurability, and alignment with Angular’s evolving ecosystem.

---

- [Bootstrapping Keycloak with `provideKeycloak`](#bootstrapping-keycloak-with-providekeycloak)
- [Migrating from `KeycloakService`](#migrating-from-keycloakservice)
- [Migrating from `KeycloakBearerInterceptor`](#migrating-from-keycloakbearerinterceptor)
- [Migrating from `KeycloakAuthGuard`](#migrating-from-keycloakauthguard)
- [Migrating from `KeycloakEvents` RxJS Subject](#migrating-from-keycloakevents-rxjs-subject)
- [Deprecation of `KeycloakAngularModule`](#deprecation-of-keycloakangularmodule)

---

## Bootstrapping Keycloak with `provideKeycloak`

The new method for bootstrapping Keycloak in your application uses the `provideKeycloak` function. This approach integrates seamlessly with Angular’s `ApplicationConfig`, allowing for cleaner organization and enhanced flexibility.

### Example

```typescript
export const appConfig: ApplicationConfig = {
providers: [
provideKeycloak({
config: {
url: 'keycloak-server-url',
realm: 'realm-id',
clientId: 'client-id'
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`
}
}),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};
```

**Key highlights:**

- `provideKeycloak` initializes the Keycloak client and optionally accepts additional providers for modular configuration.
- Additional providers can help organize configurations, such as loading Keycloak settings from a separate file.

** Additional Resources**
For more details, refer to the [Keycloak Provider](../provider.md) documentation.

## Migrating from `KeycloakService`

The `KeycloakService` wrapper is no longer required in Keycloak Angular v19. Instead, the library provides direct access to the `keycloak-js` client instance using Angular’s `inject()` function. This new approach simplifies usage and provides greater flexibility for future updates to `keycloak-js`.

### Accessing the Keycloak Client

You can now inject the `keycloak-js` client instance directly into your components, services, or other Angular classes using `inject(Keycloak)`. This method ensures compatibility with updates to `keycloak-js` while reducing dependency on custom wrappers.

### Example

```typescript
import { Component, inject } from '@angular/core';
import Keycloak from 'keycloak-js';

@Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.css']
})
export class MenuComponent {
private readonly keycloak = inject(Keycloak);

login() {
this.keycloak.login();
}

logout() {
this.keycloak.logout();
}
}
```

**Key Benefits:**

- **Direct Access:** The inject(Keycloak) approach gives you direct access to the keycloak-js client without requiring an intermediate wrapper.
- **Future-proofing:** Reduces dependencies on library-specific abstractions, making it easier to adapt to updates or changes in keycloak-js.
- **Simpler Code:** Eliminates the need for additional services, resulting in cleaner and more maintainable code.

## Migrating from KeycloakBearerInterceptor

## Migrating from `KeycloakBearerInterceptor`

In previous versions of Keycloak Angular, the `KeycloakBearerInterceptor` was automatically included when importing the `KeycloakAngularModule`. Starting with version 19, the library no longer adds any interceptors automatically. All interceptor configurations must now be defined declaratively, giving full control to the application to decide the implementation and configuration.

### Key Changes

1. **No Automatic Interceptors**:
- In v19, interceptors are not added by default. This ensures that the application explicitly configures them, avoiding unintentional behaviors or security risks.
2. **Deprecation of `ExcludeUrls` Configuration**:
- The previous approach, which allowed excluding certain URLs via the `ExcludeUrls` configuration, has been removed. This change addresses a critical issue where Bearer tokens could unintentionally be sent to external services if the `ExcludeUrls` were improperly configured, potentially compromising security.
- Instead, applications must now specify the URLs where the Authorization Bearer token should be added. This is handled using declarative interceptor configurations.
3. **Decoupled Configurations**:
- Interceptor configurations are no longer tied to the `KeycloakService` configuration. Instead, each interceptor uses specific injection tokens for its configuration, promoting modularity and flexibility.

### Example

```typescript
import { provideKeycloak } from 'keycloak-angular';
import {
createInterceptorCondition,
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
includeBearerTokenInterceptor
} from 'keycloak-angular';
import { ApplicationConfig, provideRouter } from '@angular/router';
import { provideZoneChangeDetection, provideHttpClient, withInterceptors } from '@angular/core';

const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(http:\/\/localhost:8181)(\/.*)?$/i // Match URLs starting with http://localhost:8181
});

export const provideKeycloakAngular = () =>
provideKeycloak({
config: {
realm: 'keycloak-angular-sandbox',
url: 'http://localhost:8080',
clientId: 'keycloak-angular'
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
redirectUri: window.location.origin + '/'
},
providers: [
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [localhostCondition] // Specify conditions for adding the Bearer token
}
]
});

export const appConfig: ApplicationConfig = {
providers: [
provideKeycloakAngular(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptors([includeBearerTokenInterceptor])) // Add the configured interceptor
]
};
```

**Key highlights:**

- Custom Interceptor Configuration:
- The includeBearerTokenInterceptor requires the INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG injection token.
- The configuration accepts an array of conditions, allowing customization based on URL patterns, HTTP methods, or other criteria.
- Security by Design:
- Only explicitly defined URLs will include the Bearer token, reducing the risk of exposing tokens to unintended endpoints.
- Flexibility:
- The declarative approach enables advanced scenarios, such as using different headers or Bearer prefixes for specific endpoints.

** Additional Resources**
For more details on the available interceptors and their configurations, refer to the [Keycloak HttpClient Interceptors](../interceptors.md) documentation.

## Migrating from `KeycloakAuthGuard`

In previous versions of Keycloak Angular, the `KeycloakAuthGuard` was a class that required the application to implement the logic for granting or denying access to routes. This concept remains in version 19, but it has been modernized with the introduction of the `createAuthGuard` higher-order function. This function allows you to create functional route guards using Angular's `CanActivateFn` or `CanActivateChildFn`.

### Key Changes

1. **Higher-Order Function**:
- The `KeycloakAuthGuard` class is replaced by `createAuthGuard`, which generates functional guards.
- This aligns with Angular's move toward functional APIs, simplifying guard creation and usage.
2. **Authentication Data**:
- The new `createAuthGuard` function provides an `AuthGuardData` object containing:
- The Keycloak instance.
- The `authenticated` flag.
- The user's granted roles.
3. **Flexibility**:
- Guards can now be implemented as concise and reusable functions.

### Example

```typescript
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { inject } from '@angular/core';
import { createAuthGuard, AuthGuardData } from 'keycloak-angular';

const isAccessAllowed = async (
route: ActivatedRouteSnapshot,
_: RouterStateSnapshot,
authData: AuthGuardData
): Promise<boolean | UrlTree> => {
const { authenticated, grantedRoles } = authData;

const requiredRole = route.data['role'];
if (!requiredRole) {
return false;
}

const hasRequiredRole = (role: string): boolean =>
Object.values(grantedRoles.resourceRoles).some((roles) => roles.includes(role));

if (authenticated && hasRequiredRole(requiredRole)) {
return true;
}

const router = inject(Router);
return router.parseUrl('/forbidden');
};

export const canActivateAuthRole = createAuthGuard<CanActivateFn>(isAccessAllowed);
```

**Key Highlights**

1. **Access Logic:**
- The isAccessAllowed function handles the access control logic based on route data (role), user authentication status, and granted roles.
2. **Injection of Angular Services:**
- Angular's inject() function can be used to access services like Router within the guard logic.
3. **Customizable Behavior:**
- Guards can return a UrlTree (e.g., router.parseUrl('/forbidden')) to redirect unauthorized users.
4. **Reusability:**
- The guard logic (isAccessAllowed) is separated from its implementation, making it reusable across multiple routes.

## Migrating from `KeycloakEvents` RxJS Subject

In previous versions of Keycloak Angular, Keycloak events were managed using an RxJS `Subject`. This approach was often cumbersome and error-prone. In version 19, Keycloak events have been refactored to use Angular Signals, providing a cleaner, more reactive, and intuitive API for handling events.

### Key Changes

1. **Angular Signals**:
- Keycloak events are now exposed as Angular Signals, offering a more declarative and reactive way to handle client events.
2. **Early Initialization**:
- Keycloak events are instantiated at the start of the application lifecycle, before the Keycloak client is initialized.
3. **Injection Token**:
- The events are injected via the `KEYCLOAK_EVENT_SIGNAL` injection token, ensuring seamless integration and availability wherever needed.

### Example

```typescript
import { Component, effect, inject } from '@angular/core';
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, typeEventArgs, ReadyArgs } from 'keycloak-angular';

@Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.css']
})
export class MenuComponent {
authenticated = false;
private readonly keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL);

constructor() {
effect(() => {
const keycloakEvent = this.keycloakSignal();

if (keycloakEvent.type === KeycloakEventType.Ready) {
this.authenticated = typeEventArgs<ReadyArgs>(keycloakEvent.args);
}

if (keycloakEvent.type === KeycloakEventType.AuthLogout) {
this.authenticated = false;
}
});
}
}
```

**Key Highlights:**

1. **Declarative Event Handling:**
- The effect function reacts automatically to changes in the Signal, eliminating the need to manually subscribe and unsubscribe.
2. **Improved Readability:**
- Event handling logic is more concise and integrates seamlessly with Angular's reactive programming model.
3. **Strong Typing:**
- The typeEventArgs utility ensures that event arguments are strongly typed, reducing runtime errors and improving developer experience.
4. **Early Event Availability:**
- Events are available immediately after application startup, even before the Keycloak client is fully initialized.

## Deprecation of `KeycloakAngularModule`

While the previous implementation of Keycloak Angular remains available in version 19, it is highly recommended to transition to the new architecture as soon as possible. The older implementation will be removed in a future release, likely in Keycloak Angular v20.

### Key Deprecations

The following classes have been deprecated in version 19:

- `KeycloakAngularModule`
- `KeycloakService`
- `KeycloakAuthGuard`
- `KeycloakBearerInterceptor`

These classes remain available for compatibility purposes, but applications should migrate to the new APIs and approaches outlined in this guide.

### Renamed Interfaces

To avoid conflicts with the new implementation, certain interfaces have been renamed:

| Previous Name | New Name |
| :-----------------: | :-----------------------: |
| `KeycloakEvent` | `KeycloakEventLegacy` |
| `KeycloakEventType` | `KeycloakEventTypeLegacy` |
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';

import { BookService } from '../../services/book.service';
import { Book } from '../../models/book.model';
Expand All @@ -10,8 +10,7 @@ import { Book } from '../../models/book.model';
})
export class BooksComponent implements OnInit {
books: Book[] = [];

constructor(private bookService: BookService) {}
private readonly bookService = inject(BookService);

ngOnInit() {
this.bookService.listBooks().subscribe((data) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import {
export class MenuComponent {
authenticated = false;
keycloakStatus: string | undefined;
private readonly keycloak = inject(Keycloak);
private readonly keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL);

constructor(private readonly keycloak: Keycloak) {
const keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL);

constructor() {
effect(() => {
const keycloakEvent = keycloakSignal();
const keycloakEvent = this.keycloakSignal();

this.keycloakStatus = keycloakSignal().type;
this.keycloakStatus = keycloakEvent.type;

if (keycloakEvent.type === KeycloakEventType.Ready) {
this.authenticated = typeEventArgs<ReadyArgs>(keycloakEvent.args);
Expand Down
Loading

0 comments on commit 2664d0c

Please sign in to comment.