@devlearning/jwt-auth
Version:
Jwt Angular Authentication manager with automatic Refresh Token management.
229 lines (174 loc) • 5.99 kB
Markdown
# JwtAuth
JWT Angular Authentication manager with automatic Refresh Token management, multi-tab sync, and mutex-based concurrent refresh protection.
## Installation
```bash
npm i @devlearning/jwt-auth
```
## Configuration
Add `JwtAuthModule.forRoot(...)` to your `AppModule`:
```ts
import { JwtAuthModule } from '@devlearning/jwt-auth';
@NgModule({
imports: [
JwtAuthModule.forRoot({
tokenUrl: environment.jwtAuthToken,
refreshUrl: environment.jwtAuthRefreshToken,
})
]
})
export class AppModule {}
```
### JwtAuthConfig options
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| `tokenUrl` | `string` | ✅ | — | URL to obtain the bearer token |
| `refreshUrl` | `string` | ✅ | — | URL to refresh the bearer token |
| `useManualInitialization` | `boolean` | — | `false` | If `true`, you must call `init()` manually |
| `logLevel` | `JwtAuthLogLevel` | — | — | Minimum log level (`VERBOSE`, `INFO`, `WARNING`, `ERROR`, `NONE`) |
| `storageType` | `StorageType` | — | `LOCAL_STORAGE` | Storage used for token persistence (`LOCAL_STORAGE` or `SESSION_STORAGE`) |
| `refreshTokenRequestFactory` | `(token: JwtTokenBase) => object` | — | — | Custom factory to build the refresh token request body. Use when your API requires extra fields beyond `username` and `refreshToken` |
Example with all options:
```ts
JwtAuthModule.forRoot({
tokenUrl: '/api/auth/token',
refreshUrl: '/api/auth/refresh',
useManualInitialization: false,
logLevel: JwtAuthLogLevel.ERROR,
storageType: StorageType.SESSION_STORAGE,
})
```
## Token model
The server response for both `tokenUrl` and `refreshUrl` must be compatible with `JwtTokenBase`:
```ts
export class JwtTokenBase {
username: string | undefined;
accessToken: string | undefined;
expiresIn: number | undefined; // Unix timestamp (ms)
refreshToken: string | undefined;
refreshTokenExpiresIn: number | undefined; // Unix timestamp (ms)
}
```
You can extend it with your own fields:
```ts
export class MyToken extends JwtTokenBase {
email: string;
role: string;
}
```
Then pass it as the generic parameter to the service:
```ts
constructor(private readonly _jwtAuth: JwtAuthService<MyToken>) {}
```
## Usage
### Login
Call `token()` with your login request object. The method is generic so you can pass any typed request:
```ts
import { JwtAuthService } from '@devlearning/jwt-auth';
export interface LoginRequest {
username: string;
password: string;
}
@Injectable()
export class AuthService {
constructor(private readonly _jwtAuth: JwtAuthService<MyToken>) {}
login(username: string, password: string) {
return this._jwtAuth.token<LoginRequest>({ username, password });
}
}
```
### Logout
```ts
this._jwtAuth.logout();
```
### Reactive state
| Member | Type | Description |
|---|---|---|
| `isLoggedIn$` | `Observable<boolean>` | Emits whenever the login state changes |
| `jwtToken$` | `Observable<Token \| null>` | Emits whenever the token changes |
| `refreshingToken$` | `Observable<boolean>` | Emits `true` while a refresh is in progress |
| `isLoggedIn` | `boolean` | Current login state (synchronous) |
| `jwtToken` | `Token \| null` | Current token (synchronous) |
## Custom refresh token request
If your refresh endpoint requires extra fields (e.g. an application code), provide a `refreshTokenRequestFactory` in the config:
```ts
JwtAuthModule.forRoot({
tokenUrl: '/api/auth/token',
refreshUrl: '/api/auth/refresh',
refreshTokenRequestFactory: (token) => ({
authApplicationCode: 'MY_APP',
username: token.username,
refreshToken: token.refreshToken,
}),
})
```
When `refreshTokenRequestFactory` is not provided, the default request body sent to `refreshUrl` is:
```json
{
"username": "...",
"refreshToken": "..."
}
```
## Guard
Extend `JwtAuthGuard` to protect your routes:
```ts
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { JwtAuthGuard, JwtAuthService } from '@devlearning/jwt-auth';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class AuthGuard extends JwtAuthGuard implements CanActivate {
constructor(
private readonly _router: Router,
private readonly _jwtAuth: JwtAuthService<any>
) {
super(_jwtAuth);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.canActivateBase(route, state)
.pipe(
map(x => {
if (x) {
return true;
} else {
this._router.navigateByUrl('/login');
return false;
}
}),
catchError(() => {
this._router.navigateByUrl('/login');
return of(false);
})
);
}
}
```
## Manual initialization
If `useManualInitialization: true`, call `init()` at app startup (e.g. in `APP_INITIALIZER`). This will attempt to restore the session from storage, refreshing the token automatically if needed:
```ts
export function initializeAuth(jwtAuth: JwtAuthService<any>) {
return () => jwtAuth.init().toPromise();
}
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeAuth,
deps: [JwtAuthService],
multi: true,
}
]
})
export class AppModule {}
```
## Multi-tab sync
When using `LOCAL_STORAGE`, token changes in other browser tabs are automatically detected and synced via the `storage` event. This ensures all tabs share the same authentication state.
`SESSION_STORAGE` is scoped to a single tab and does not sync across tabs.