npm package: @sftech/ng-auth
Description: ![Version](https://img.shields.io/badge/version-2.0.2-blue)
Latest version: 2.1.4

# @sftech/ng-auth

![Version](https://img.shields.io/badge/version-2.0.2-blue)

Angular library for OAuth2/OIDC authentication with anonymous session fallback, guards, and HTTP interceptor.

## Installation

```bash
npm install @sftech/ng-auth
```

## Peer Dependencies

| Package | Minimum Version |
|---------|----------------|
| `@angular/common` | `>=20.0.0` |
| `@angular/core` | `>=20.0.0` |

## Configuration

Add `AuthModule.forRoot()` to your `app.config.ts`:

```typescript
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AuthModule, IAuthConfig, authenticationInterceptor } from '@sftech/ng-auth';

const authConfig: IAuthConfig = {
    authProvider: 'oauth',               // 'oauth' | 'none'
    authBackendUrl: '/api/auth',         // Backend auth API base URL
    postLogoutRedirectUri: 'http://localhost:4200',
    anonymousFallback: true,             // Optional: enable anonymous sessions (default: false)
    autoRefreshToken: true,              // Optional: enable automatic token refresh (default: true)
    refreshBeforeExpiryMinutes: 1,       // Optional: minutes before expiry to refresh (default: 1)
};

export function appConfig(): ApplicationConfig {
    return {
        providers: [
            importProvidersFrom(AuthModule.forRoot(authConfig)),
            provideHttpClient(withInterceptors([authenticationInterceptor])),
        ],
    };
}
```

### IAuthConfig Reference

```typescript
interface IAuthConfig {
    /** Authentication provider. Use 'oauth' for OAuth2/OIDC, 'none' to disable auth entirely. */
    authProvider: 'oauth' | 'none';

    /** Base URL of the backend auth API (e.g. '/api/auth' or 'https://api.example.com/auth'). */
    authBackendUrl: string;

    /** URI to redirect to after OAuth provider logout. */
    postLogoutRedirectUri?: string;

    /**
     * When true, users without an OAuth session automatically receive an anonymous session.
     * The anonymous session is created via POST /auth/anonymous. Default: false.
     */
    anonymousFallback?: boolean;

    /** When false, disables automatic token refresh scheduling. Default: true. */
    autoRefreshToken?: boolean;

    /** Minutes before token expiry to trigger a refresh. Default: 1. */
    refreshBeforeExpiryMinutes?: number;
}
```

## Routing

Add the auth routes to your application:

```typescript
import { Route } from '@angular/router';
import { authenticationGuard } from '@sftech/ng-auth';

export const appRoutes: Route[] = [
    {
        path: 'auth',
        loadChildren: () => import('@sftech/ng-auth').then((m) => m.authRoutes),
    },
    {
        path: 'protected',
        component: MyComponent,
        canActivate: [authenticationGuard],
    },
];
```

The `authRoutes` include:
- `/auth/callback` - OAuth callback handler (`CallbackComponent`)
- `/auth/recovery` - Anonymous session recovery page (`RecoveryComponent`)

## Features

- OAuth2/OIDC authentication via backend redirect flow
- Signal-based reactive authentication state (`isAuthenticated`, `isAnonymous`, `recoveryCode`)
- Anonymous session fallback with recovery code support
- Automatic token refresh scheduling based on session expiry
- Unified auth status endpoint for both OAuth and anonymous sessions
- Functional guards: `authenticationGuard`, `adminGuard`
- HTTP interceptor that adds `withCredentials: true` to auth-related requests
- Session recovery component for restoring anonymous sessions by code
- localStorage persistence of recovery codes across page refreshes

## API Reference

### Services

| Service | Description | Key Methods |
|---------|-------------|-------------|
| `AuthenticationService` | Signal-based authentication service | `checkAuthStatus()`, `login()`, `logout()`, `refreshToken()`, `recoverByCode()`, `loadUserInfo()` |

### Guards

| Guard | Type | Description |
|-------|------|-------------|
| `authenticationGuard` | `CanActivateFn` | Protects routes. Redirects to login when not authenticated. With `anonymousFallback: true`, creates an anonymous session instead. When `authProvider` is `'none'`, allows all access. |
| `adminGuard` | `CanActivateFn` | Restricts routes to users with the `admin` role. |

### Interceptors

| Interceptor | Description |
|-------------|-------------|
| `authenticationInterceptor` | `HttpInterceptorFn` that adds `withCredentials: true` to requests matching `/api/auth`, `/api/orchestrator`, or `/api/orchestrator-db`. |

### Models

| Model | Description | Factory Methods |
|-------|-------------|-----------------|
| `AuthStatus` | Represents authentication state | `fromDto()`, `fromAnonymousSessionDto()`, `notAuthenticated()`, `authenticated()` |
| `UserInfo` | OAuth user profile data | `fromDto()` |

### DTOs

| DTO | Description |
|-----|-------------|
| `IAuthStatusResponseDto` | Response from `GET /auth/status`. Covers both OAuth and anonymous sessions. |
| `IAnonymousSessionResponseDto` | Response from `POST /auth/anonymous` and `POST /auth/anonymous/recover`. Contains full session data. |
| `ILogoutResponseDto` | Response from `POST /auth/logout`. Includes optional `anonymousSession` field for fallback setup. |
| `IUserInfoResponseDto` | Response from `GET /auth/user`. OAuth user profile. |
| `ITokenRefreshResponseDto` | Response from `POST /auth/refresh`. |

### Enums

| Enum | Values | Description |
|------|--------|-------------|
| `EAuthApiEndpoint` | `STATUS`, `USER`, `LOGIN`, `LOGOUT`, `REFRESH`, `ANONYMOUS_CREATE`, `ANONYMOUS_RECOVER` | Backend endpoint path segments |
| `EAuthFailureReason` | Library-defined values | Reason codes returned when `authenticated: false` |
| `EAuthStorageKey` | `RECOVERY_CODE` | Keys used for localStorage |
| `EAuthRole` | `ADMIN` | Role values checked by `adminGuard` |

### DTO Interfaces

#### IAuthStatusResponseDto

```typescript
interface IAuthStatusResponseDto {
    authenticated: boolean;
    userId?: string;
    /** Visitor ID for anonymous sessions. Mapped to userId in AuthStatus. */
    visitorId?: string;
    expiresAt?: string;
    timeRemaining?: number;
    reason?: EAuthFailureReason;
    /** Present when the session is an anonymous session (since 2.0.0). */
    isAnonymous?: boolean;
    /** Recovery code for anonymous sessions, returned when isAnonymous is true (since 2.0.0). */
    recoveryCode?: string;
}
```

#### IAnonymousSessionResponseDto

```typescript
interface IAnonymousSessionResponseDto {
    authenticated: boolean;
    isAnonymous: boolean;
    userId: string;
    visitorId: string;
    recoveryCode: string;
    sessionId: string;
    expiresAt: string;
}
```

#### ILogoutResponseDto

```typescript
interface ILogoutResponseDto {
    message: string;
    providerLogoutUrl: string;
    success: boolean;
    /** Anonymous session returned by the backend after OAuth logout when anonymousFallback is true (since 2.0.0). */
    anonymousSession?: {
        sessionId: string;
        userId: string;
        visitorId: string;
        recoveryCode: string;
        expiresAt: string;
    };
}
```

## Usage Examples

### Basic Setup (OAuth only)

```typescript
// app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AuthModule, IAuthConfig, authenticationInterceptor } from '@sftech/ng-auth';

const authConfig: IAuthConfig = {
    authProvider: 'oauth',
    authBackendUrl: '/api/auth',
};

export function appConfig(): ApplicationConfig {
    return {
        providers: [
            importProvidersFrom(AuthModule.forRoot(authConfig)),
            provideHttpClient(withInterceptors([authenticationInterceptor])),
        ],
    };
}
```

### Advanced: Anonymous Session Fallback

When `anonymousFallback: true` is set, unauthenticated users automatically receive an anonymous session. This is useful for applications that want to track anonymous activity before users log in.

```typescript
// app.config.ts
const authConfig: IAuthConfig = {
    authProvider: 'oauth',
    authBackendUrl: '/api/auth',
    anonymousFallback: true,
};
```

```typescript
// my.component.ts
import { Component, inject } from '@angular/core';
import { AuthenticationService } from '@sftech/ng-auth';

@Component({
    selector: 'app-my',
    template: `
        <div *ngIf="auth.isAnonymous()">
            You are browsing anonymously.
            Recovery code: {{ auth.recoveryCode() }}
        </div>
        <div *ngIf="auth.isAuthenticated() && !auth.isAnonymous()">
            Welcome, {{ auth.currentUser()?.name }}!
        </div>
    `,
})
export class MyComponent {
    protected auth = inject(AuthenticationService);
}
```

### Advanced: Using AuthenticationService Signals

```typescript
import { Component, inject } from '@angular/core';
import { AuthenticationService } from '@sftech/ng-auth';

@Component({ ... })
export class MyComponent {
    private auth = inject(AuthenticationService);

    // Signals — use in templates or effects
    isAuthenticated = this.auth.isAuthenticated;  // Signal<boolean>
    isAnonymous = this.auth.isAnonymous;          // Signal<boolean>
    userId = this.auth.userId;                    // Signal<string | undefined>
    currentUser = this.auth.currentUser;          // Signal<UserInfo | null>
    recoveryCode = this.auth.recoveryCode;        // Signal<string | null>
    timeRemaining = this.auth.timeRemaining;      // Signal<number | undefined>

    login() { this.auth.login(); }
    logout() { this.auth.logout().subscribe(); }
}
```

### Advanced: Session Recovery

Users with an anonymous session can obtain a recovery code from `auth.recoveryCode()`. If they lose their session cookie (e.g., different browser), they can restore the session:

```typescript
import { Component, inject } from '@angular/core';
import { AuthenticationService } from '@sftech/ng-auth';

@Component({ ... })
export class RecoveryPageComponent {
    private auth = inject(AuthenticationService);

    recover(code: string) {
        this.auth.recoverByCode(code).subscribe((status) => {
            if (status.authenticated) {
                // Session restored successfully
            }
        });
    }
}
```

The built-in `RecoveryComponent` (available at the `/auth/recovery` route) handles this flow automatically.

### Advanced: Protecting Admin Routes

```typescript
import { Route } from '@angular/router';
import { authenticationGuard, adminGuard } from '@sftech/ng-auth';

export const appRoutes: Route[] = [
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [authenticationGuard, adminGuard],
    },
];
```

## Auth Flows

### 1. OAuth Login

```
login() → GET /auth/login?redirect_url=<encoded-origin>/auth/callback → OAuth provider → callback → GET /auth/status → GET /auth/user
```

The `GET /auth/status` endpoint is the single unified status check for both OAuth and anonymous sessions.

### 2. Anonymous Fallback (initial page load, no session)

```
GET /auth/status → { authenticated: false }
→ POST /auth/anonymous → { authenticated: true, isAnonymous: true, recoveryCode: "..." }
```

The `POST /auth/anonymous` response contains all session data needed to construct `AuthStatus` directly — no follow-up status call is made.

### 3. Resuming Existing Anonymous Session

```
GET /auth/status → { authenticated: true, isAnonymous: true, visitorId: "...", recoveryCode: "..." }
```

A single status call detects the existing anonymous session. The `visitorId` field is mapped to `userId` in the `AuthStatus` model.

### 4. OAuth Logout with Anonymous Fallback

```
POST /auth/logout → { success: true, anonymousSession: { ... } }
→ AuthStatus constructed from embedded anonymousSession (no additional calls)
```

If `anonymousSession` is absent from the logout response (backend version compatibility), a `POST /auth/anonymous` call is made as a fallback.

### 5. Anonymous Logout (reset identity)

```
clearAuthState()
→ POST /auth/anonymous → { authenticated: true, isAnonymous: true, ... }
```

There is no `/auth/anonymous/logout` endpoint. Anonymous logout clears local state and creates a fresh session.

### 6. Token Refresh

```
POST /auth/refresh  (OAuth sessions)
GET /auth/status    (anonymous sessions — re-checks the unified endpoint)
```

Refresh is scheduled automatically based on `timeRemaining` from the status response. Configure with `autoRefreshToken` and `refreshBeforeExpiryMinutes`.

### 7. Session Recovery

```
POST /auth/anonymous/recover { recoveryCode } → { authenticated: true, isAnonymous: true, ... }
```

The response contains full session data. `AuthStatus` is constructed directly without a follow-up status call.

## API Requirements

The library expects the following endpoints on the backend at the configured `authBackendUrl`:

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/status` | Unified auth status. Returns OAuth or anonymous session data. |
| `GET` | `/user` | OAuth user profile. Only called when `authenticated: true` and not anonymous. |
| `GET` | `/login` | Initiates OAuth login redirect. Accepts optional `redirect_url` query parameter (URL-encoded callback URL). When provided, the backend redirects to this URL after OAuth completion instead of its own configured `frontendCallbackUrl`. Required for Capacitor environments. |
| `POST` | `/logout` | OAuth logout. Returns optional `anonymousSession` for fallback. |
| `POST` | `/refresh` | Refreshes OAuth access token. |
| `POST` | `/anonymous` | Creates a new anonymous session. Returns full session data. |
| `POST` | `/anonymous/recover` | Recovers an anonymous session by recovery code. Returns full session data. |

## Migration from 1.x

Version 2.0.0 contains breaking changes. If you are upgrading from 1.x, review the following:

### Removed Exports

| Symbol | Action Required |
|--------|----------------|
| `IAnonymousLogoutResponseDto` | Removed. The `/auth/anonymous/logout` endpoint does not exist in the backend. No replacement needed — anonymous logout is now client-side only. |
| `EAuthApiEndpoint.ANONYMOUS_STATUS` | Removed. Use the unified `EAuthApiEndpoint.STATUS` (`GET /auth/status`) for all session status checks. |
| `EAuthApiEndpoint.ANONYMOUS_LOGOUT` | Removed. Anonymous logout does not call any backend endpoint. |
| `AuthStatus.fromAnonymousStatusDto()` | Removed. Use `AuthStatus.fromDto()` — it now detects anonymous sessions via the `isAnonymous` flag automatically. |

### Changed Behavior

| Area | Change |
|------|--------|
| `IAuthStatusResponseDto` | Extended with `isAnonymous?: boolean` and `recoveryCode?: string` fields. The unified `GET /auth/status` endpoint now returns these fields for anonymous sessions. |
| `IAnonymousSessionResponseDto` | Expanded from a minimal shape to include `authenticated`, `isAnonymous`, `userId`, `visitorId`, `recoveryCode`, `sessionId`, and `expiresAt`. |
| `ILogoutResponseDto` | Extended with an optional `anonymousSession` field. When `anonymousFallback: true`, the backend embeds the new anonymous session in the logout response. |
| `AuthStatus.fromDto()` | Now detects anonymous sessions via the `isAnonymous` flag in the response and maps `visitorId` to `userId` automatically. No code changes needed if you were calling `checkAuthStatus()`. |
| `createAnonymousSession()` | Uses the `POST /auth/anonymous` response directly to build `AuthStatus` — no follow-up GET call. |
| `recoverByCode()` | Uses the `POST /auth/anonymous/recover` response directly — no follow-up GET call. |
| `logout()` (OAuth) | Reads `anonymousSession` from the logout response when `anonymousFallback: true` — no additional `POST /auth/anonymous` call in the happy path. |
| Angular peer dependency | Bumped from `>=19.0.0` to `>=20.0.0`. |

### Migration Example

**Before (1.x):**
```typescript
// If you referenced removed enum values or DTOs directly:
import {
    EAuthApiEndpoint,
    IAnonymousLogoutResponseDto,
    AuthStatus,
} from '@sftech/ng-auth';

// ANONYMOUS_STATUS and ANONYMOUS_LOGOUT no longer exist:
const anonStatusUrl = EAuthApiEndpoint.ANONYMOUS_STATUS; // ERROR in 2.0.0
const anonLogoutUrl = EAuthApiEndpoint.ANONYMOUS_LOGOUT; // ERROR in 2.0.0

// fromAnonymousStatusDto() no longer exists:
const status = AuthStatus.fromAnonymousStatusDto(dto);   // ERROR in 2.0.0
```

**After (2.0.0):**
```typescript
import { EAuthApiEndpoint, AuthStatus } from '@sftech/ng-auth';

// Use unified STATUS for all session checks:
const statusUrl = EAuthApiEndpoint.STATUS; // '/status'

// fromDto() now handles anonymous sessions automatically:
const status = AuthStatus.fromDto(dto);    // Works for OAuth and anonymous
// Or use fromAnonymousSessionDto() for POST /auth/anonymous responses:
const anonStatus = AuthStatus.fromAnonymousSessionDto(anonDto);
```

## Development

### Build

```bash
npx nx build ng-auth
```

### Test

```bash
npx nx test ng-auth
```

### Lint

```bash
npx nx lint ng-auth
```

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)

## License

MIT