npm package: @ndwnu/auth
Description: This package provides some tools to ease the implementation of authentication in Angular applications and uses [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc/tree/master) under the hood. It aims to simplify and improve developm
Latest version: 4.0.1

# @ndwnu/auth

This package provides some tools to ease the implementation of authentication in Angular applications and uses [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc/tree/master) under the hood.
It aims to simplify and improve development by offering a service, multiple guards and an easy installation process for your new or existing Angular applications.

## Features

- 🚀 **One config installation**, by utilizing a custom EnvironmentProviders and the Angular Injection Context we can simplify the installation process and avoid the need of manually configuring the OAuth process.
- 🛡️ **CanActivate Guard**, we provide a guard to cover most of the use cases, including:
  - Force a login
  - Role based access
  - Custom callback function to handle unauthorized access
- 🔐 **NdwAuthService**: A service that provides a simple interface to interact with the OAuth process, including login, logout, and different observables like a user profile.
- ☎️ **Default onUnauthorized** (optional) for all the AuthGuards, define once and it is used automatically in all the guards.

## Before you start

**This package uses the standalone APIs** which are introduced in Angular 14. You can migrate your project easily using the schematic provided by Angular.
Start migrating your project: https://angular.dev/reference/migrations/standalone

## Installation

```bash
  npm install @ndwnu/auth
```

## Usage

After installing the package, you can use `provideNdwAuth` to configure authentication in your application.
This provider will automatically configure `OAuth` and initializes it using the provideAppInitializer.

### Basic usage

To use the `provideNdwAuth` provider, you need to import it in your `app.config.ts` file and provide it in the `providers` array.

```typescript
// app.config.ts
import { provideNdwAuth } from '@ndwnu/auth';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNdwAuth({
      authConfig: {
        issuer: 'https://iam.test.com/auth/realms/my-realm',
        redirectUri: window.location.origin,
        postLogoutRedirectUri: window.location.origin,
        clientId: 'app-client-id',
        responseType: 'code',
        scope: 'openid profile email roles',
        showDebugInformation: false,
      },
      moduleConfig: {
        resourceServer: {
          allowedUrls: ['http://localhost:4200'],
          sendAccessToken: true,
        },
      },
    }),
    provideHttpClient(withInterceptorsFromDi()), // optional
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
  ],
};

// main.ts
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
```

### onUnauthorized

The `onUnauthorized` function is called when the user is not authenticated or does not have the required roles. You can use this function to redirect the user to a login page or show an error message.
It runs in the injection context so you can inject your own services. The callback function receives a parameter of type `UnauthorizedType` which can be either `forbidden` or `unauthenticated`.
`Forbidden` means that the user is authenticated but does not have the required roles, while `unauthenticated` means that the user is not authenticated.

Defining the onUnauthorized callback function is optional, but it is recommended to define one to handle unauthorized access in a consistent way across your application.
You can still override the onUnauthorized function in the guards if you want to handle it differently in a specific route.

```typescript
import { NdwAuthService, UnauthorizedType } from '@ndwnu/auth';
import { Router } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNdwAuth({
      ...,
      onUnauthorized: (type) => {
        if (type === 'forbidden') {
          window.alert('You do not have permission to access this page.');
        } else if (type === 'unauthenticated') {
          window.alert('You are not authenticated. Please log in.');
          inject(Router).navigate(['/login']);
        }
      },
    }),
  ],
};

```

### Custom Storage

You can also use a custom storage implementation by providing a new provideNdwAuthStorage provider. This will overwrite the default storage implementation.
By default this implementation uses localStorage, but you can use any storage implementation that implements the `OAuthStorage` interface.

```typescript
import { provideNdwAuth } from '@ndwnu/auth';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNdwAuth({ ... }),
    provideNdwAuthStorage({
      factory: () => {
        return new CustomStorage();
      },
      deps: [] // Optional, you can provide dependencies here
    }),
  ]
};
```

### Using Interceptors

We currently do not provide the interceptors by default, but you can use the `withInterceptorsFromDi` function to add the interceptors to your application.
We do this because we want to avoid duplicating the HttpClient providers, as it causes an issue known as 'provider shadowing'.

In Angular's dependency injection system:

- The last registered provider wins
- The providers are processed in order

This means that either:

- Your custom HTTP client configuration in appConfig will override the one provided by provideNdwAuth
- Or provideNdwAuth's HTTP client will override yours, depending on the order of registration

This could lead to:

- Missing interceptors if they're registered in only one of the providers
- Unexpected behavior where some services use one HTTP client and others use another

Example on how to use:

```typescript
import { provideNdwAuth } from '@ndwnu/auth';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNdwAuth({ ... }),
    provideHttpClient(withInterceptorsFromDi()),
  ]
};
```

## NdwAuthService

You can use the `NdwAuthService` to login and logout the user, check roles and to get the user profile.

### Login and logout

```typescript
import { NdwAuthService } from '@ndwnu/auth';
import { Component, inject } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <button (click)="login()">Login</button>
    <button (click)="logout()">Logout</button>
  `,
})
export class LoginComponent {
  readonly #ndwAuthService = inject(NdwAuthService);

  login() {
    this.#ndwAuthService.login();
  }

  logout() {
    this.#ndwAuthService.logout();
  }
}
```

### Checking roles

You can use the `hasRole` method to check if the user has a specific role.

```typescript
import { NdwAuthService } from '@ndwnu/auth';
// ...
readonly #ndwAuthService = inject(NdwAuthService);
// ...
if (this.#ndwAuthService.hasRoles(['admin'])) {
  // User has the admin role
} else {
  // User does not have the admin role
}
```

You can also check if the user has all the required roles or if the user has at least one of the required roles.

```typescript
this.#ndwAuthService.hasRoles(['admin', 'editor'], 'all'); // Default, User needs to have both 'admin' and 'editor' roles
this.#ndwAuthService.hasRoles(['admin', 'writer'], 'any'); // User needs to have at least one of the provided roles
```

### User profile

You can use the `userProfile$` observable to get the user profile. This observable will emit the user profile when the user is authenticated and will emit `null` when the user is not authenticated.

```typescript
import { NdwAuthService } from '@ndwnu/auth';
import { Component, inject } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `
    @let userProfile = ndwAuthService.userProfile$ | async;
    @if (userProfile) {
      <p>Logged in as {{ userProfile.name }}</p>
    } @else {
      <p>Not logged in</p>
    }
  `,
})
export class UserProfileComponent {
  readonly ndwAuthService = inject(NdwAuthService);
}
```

### Observables: isAuthenticated\$, isDoneLoading\$ & isDoneAuthenticating\$

You might need to check if the user is authenticated or if the authentication process is done loading. You can use the `isAuthenticated$` and `isDoneLoading$` observables to do this.

```typescript
import { NdwAuthService } from '@ndwnu/auth';
// ...
readonly #ndwAuthService = inject(NdwAuthService);
// ...
this.#ndwAuthService.isAuthenticated$.subscribe((isAuthenticated) => {
  if (isAuthenticated) {
    // User is authenticated
  }
});
// ...
this.#ndwAuthService.isDoneLoading$.subscribe((isDoneLoading) => {
  if (isDoneLoading) {
    // The authentication process is done loading
  }
});
// Use this observable to check if the authentication process is done loading
this.#ndwAuthService.isDoneAuthenticating$.subscribe((isDone) => {
  if (isDone) {
    // User is authenticated & the authentication process is done loading
  }
});
```

## Guards

You can use the `ndwAuthGuard` guard to protect your routes. This guard will check if the user is authenticated and will trigger functionality based on your config.
However you can still create your own Guards to protect your routes, but you will need to implement the logic yourself.

The guard has the following options:

- `forceLogin`: A boolean that indicates if the user should be forced to the OAuth login if not authenticated. The default is `true`.
- `onUnauthorized`: This is a callback function that will be called when the user is not authenticated or does not have the required roles. You can use this function to redirect the user to a login page or show an error message. This overrides the default onUnauthorized function provided by `provideNdwAuth`.
- `roles`': An array of roles that the user must have to access the route. This is optional, but if you provide it, the guard will check if the user has the required roles.
- `checkRoles`: Options are `'all'`, `'any'`, This will check if the user has all the required roles or at least one of the required roles. The default is `'all'`.

#### onUnauthorized

The `onUnauthorized` function is called when the user is not authenticated or does not have the required roles. You can use this function to redirect the user to a login page or show an error message.
It runs in the injection context so you can inject your own services. The callback function receives a parameter of type `UnauthorizedType` which can be either `forbidden` or `unauthorized`.
`Forbidden` means that the user is authenticated but does not have the required roles, while `unauthenticated` means that the user is not authenticated.

The onAuthorized function in the guard will override the default onUnauthorized function provided by `provideNdwAuth` for the specific route.

### ndwAuthGuard

```typescript
import { ndwAuthGuard, NdwAuthService, UnauthorizedState } from '@ndwnu/auth';
import { Router } from '@angular/router';

export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () => import('./features/admin/admin.component').then((m) => m.AdminComponent),
    canActivate: [
      ndwAuthGuard({
        forceLogin: true, // Forces the user to login if not authenticated
        roles: ['admin', 'manager'], // The roles required to access the route
        checkRoles: 'any', // The user must have at least one of the required roles
        onUnauthorized: (type: UnauthorizedState) => {
          if (type === 'forbidden') {
            window.alert('You do not have permission to access this page.');
            inject(Router).navigate(['/public']);
          } else if (type === 'unauthenticated') {
            window.alert('You are not authenticated. Please log in.');
            inject(NdwAuthService).login();
          }
        },
      }),
    ],
  },
];
```

### Create a reusable guard

You can create a reusable guard by using the `ndwAuthGuard` function and passing it to the `canActivate` property of your route.

```typescript
import { NdwAuthService, ndwAuthGuard } from '@ndwnu/auth';

const myDefaultGuard = ndwAuthGuard({
  forceLogin: true,
  onUnauthorized: () => {
    window.alert('You are not authorized to access this page. Please log in.');
    inject(NdwAuthService).logout();
  },
});

const routes = [
  {
    path: 'admin',
    loadComponent: () => import('./features/admin/admin.component').then((m) => m.AdminComponent),
    canActivate: [myDefaultGuard],
  },
  {
    path: 'secret-page',
    loadComponent: () =>
      import('./features/secret/secret.component').then((m) => m.SecretComponent),
    canActivate: [myDefaultGuard],
  },
];
```