@ndwnu/auth
v3.0.0
Published
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
Keywords
Readme
@ndwnu/auth
This package provides some tools to ease the implementation of authentication in Angular applications and uses angular-oauth2-oidc 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
npm install @ndwnu/authUsage
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.
// 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.
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.
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:
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
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.
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.
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 rolesUser 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.
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.
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 istrue.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 byprovideNdwAuth.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
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.
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],
},
];