@mridang/nestjs-auth
Version:
A comprehensive Auth.js integration for NestJS applications with TypeScript support, framework-agnostic HTTP adapters, and role-based access control
271 lines • 12.6 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Inject, Logger, mixin, Optional, UnauthorizedException } from '@nestjs/common';
import { HttpAdapterHost, Reflector } from '@nestjs/core';
import { Auth, createActionURL, setEnvDefaults } from '@auth/core';
import memoize from 'memoize';
import { AdapterFactory } from './utils/adapter.factory.js';
import { defaultOptions } from './options.js';
import { AuthModuleOptions } from './auth-module.options.js';
import { IS_PUBLIC_KEY } from './auth.decorators.js';
import { AuthSession as AuthSessionClass } from './auth.session.js';
const NO_STRATEGY_ERROR = 'Auth.js module must be imported to use AuthGuard';
const authLogger = new Logger('AuthGuard');
function createAuthGuard(type) {
let MixinAuthGuard = class MixinAuthGuard {
reflector;
options;
adapterHost;
httpAdapter;
/**
* Initializes the guard with dependencies.
* @param reflector The `Reflector` for reading metadata.
* @param options The module options provided at initialization.
* @param adapterHost The host for the underlying HTTP adapter.
*/
constructor(reflector, options, adapterHost) {
this.reflector = reflector;
this.options = options;
this.adapterHost = adapterHost;
if (!this.options && !type) {
authLogger.error(NO_STRATEGY_ERROR);
}
}
/**
* Determines if a route can be activated by verifying the user's session.
* This method serves as the main entry point for the guard and delegates
* to a public or protected route handler based on decorator metadata.
* @param context The NestJS `ExecutionContext` for the current request.
* @returns A `Promise<boolean>` that is always `true` on successful execution.
* Access control is handled by throwing exceptions on failure.
* @throws {UnauthorizedException} If a protected route is accessed without a valid session.
*/
async canActivate(context) {
if (context.getType() !== 'http') {
return true;
}
const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]);
if (isPublic) {
return this.handlePublicRoute(context);
}
else {
return this.handleProtectedRoute(context);
}
}
/**
* Retrieves the framework-native request object from the execution context.
* @param context The `ExecutionContext` for the current request.
* @returns The request object, typed as `AuthenticatedRequest`.
*/
getRequest(context) {
return this.getOrCreateAdapter().getRequest(context);
}
// noinspection JSUnusedGlobalSymbols
/**
* Retrieves the framework-native response object from the execution context.
* @param context The `ExecutionContext` for the current request.
* @returns The raw response object.
*/
getResponse(context) {
return this.getOrCreateAdapter().getResponse(context);
}
/**
* Handles the result of an authentication attempt. It is responsible for
* throwing an exception if authentication fails or the user is not found.
* @param err An error object if an exception occurred.
* @param user The user object from the session if successful.
* @returns The validated `CoreUser` object.
* @throws {UnauthorizedException} If `err` is present or `user` is null.
*/
handleRequest(err, user) {
if (err) {
authLogger.error(`Authentication error: ${err.message}`);
}
if (err instanceof UnauthorizedException) {
throw err;
}
if (err) {
throw new UnauthorizedException(err.message);
}
if (!user) {
throw new UnauthorizedException('No user found in session');
}
return user;
}
/**
* A hook to retrieve strategy-specific options for an authentication attempt.
* Can be overridden in a child class to provide dynamic options.
* @param _context The `ExecutionContext` for the current request.
* @returns `IAuthModuleOptions` or `undefined`.
*/
getAuthenticateOptions(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_context) {
return undefined;
}
/**
* Lazily creates and retrieves the appropriate HTTP adapter (Express or Fastify).
* @returns The singleton instance of the `HttpAdapter`.
*/
getOrCreateAdapter() {
if (!this.httpAdapter) {
if (!this.adapterHost?.httpAdapter) {
throw new Error('No HTTP adapter found. Ensure app.init() is called.');
}
this.httpAdapter = AdapterFactory.create(this.adapterHost);
}
return this.httpAdapter;
}
/**
* Processes requests for routes marked as public.
* This method ensures that public routes are always accessible. It will
* attempt to retrieve the current session and attach it to the request
* for optional use, but will never throw an error.
* @param context The `ExecutionContext` for the current request.
* @returns A promise that always resolves to `true`, granting access.
*/
async handlePublicRoute(context) {
const request = this.getRequest(context);
const mergedOptions = {
providers: [],
...defaultOptions,
...this.options,
...(await this.getAuthenticateOptions(context))
};
const [coreSession] = await this.getSessionOrError(request, mergedOptions);
const publicSession = AuthSessionClass.fromCore(coreSession)?.toJSON() ?? null;
const property = mergedOptions.property ?? defaultOptions.property;
Object.assign(request, {
// keep core session on the request to match AuthenticatedRequest
session: coreSession,
// expose user from the mapped public session
[property]: publicSession?.user ?? null
});
return true;
}
/**
* Processes requests for routes that are protected (not public).
* This method enforces authentication by requiring a valid session. It throws an
* `UnauthorizedException` if authentication fails.
* @param context The `ExecutionContext` for the current request.
* @returns A promise that always resolves to `true`, as access denial is
* handled by throwing an exception.
* @throws {UnauthorizedException} If authentication fails.
*/
async handleProtectedRoute(context) {
const request = this.getRequest(context);
const response = this.getResponse(context);
const mergedOptions = {
providers: [],
...defaultOptions,
...this.options,
...(await this.getAuthenticateOptions(context))
};
const [coreSession, error] = await this.getSessionOrError(request, mergedOptions);
const publicSession = AuthSessionClass.fromCore(coreSession)?.toJSON();
if (!publicSession?.user || error) {
const acceptsHtml = request.headers?.accept?.includes('text/html');
if (acceptsHtml) {
const callbackUrl = encodeURIComponent(request.url || '/');
const signInUrl = mergedOptions.pages?.signIn || '/auth/signin';
const redirectUrl = `${signInUrl}?callbackUrl=${callbackUrl}`;
const adapter = this.getOrCreateAdapter();
adapter.setStatus(response, 302);
adapter.setHeader(response, 'Location', redirectUrl);
adapter.send(response, '');
return false;
}
else {
this.handleRequest(error, publicSession?.user ?? null);
return false;
}
}
else {
const user = this.handleRequest(error, publicSession?.user ?? null);
const property = mergedOptions.property ?? defaultOptions.property;
Object.assign(request, {
session: coreSession,
[property]: user
});
return true;
}
}
/**
* A functional wrapper around `getSession` to handle errors without a `try/catch`
* block at the call site.
* @param request The framework-native request object.
* @param options The merged Auth.js options.
* @returns A promise resolving to a tuple of `[Session | null, Error | null]`.
*/
async getSessionOrError(request, options) {
try {
const session = await this.getSession(request, options);
return [session, null];
}
catch (err) {
return [null, err instanceof Error ? err : new Error(String(err))];
}
}
/**
* The core method to retrieve a session by calling the `Auth` function from `@auth/core`.
* @param request The framework-native request object.
* @param options The merged Auth.js options.
* @returns A promise resolving to the `Session` object or `null`.
*/
async getSession(request, options) {
const adapter = this.getOrCreateAdapter();
if (!options.providers?.length) {
throw new Error('No authentication providers configured');
}
if (!options.secret && process.env.NODE_ENV === 'production') {
throw new Error('AUTH_SECRET is required in production');
}
setEnvDefaults(process.env, options);
const protocol = adapter.getProtocol(request);
const host = adapter.getHost(request);
const headers = adapter.getHeaders(request);
const url = createActionURL('session', protocol, new Headers({
host,
'x-forwarded-host': headers['x-forwarded-host'] ?? host,
'x-forwarded-proto': headers['x-forwarded-proto'] ?? protocol
}), process.env, options);
const cookieHeader = adapter.getCookie(request) ?? '';
const response = await Auth(new Request(url, { headers: { cookie: cookieHeader } }), options);
if (!response.ok) {
return null;
}
try {
return (await response.json());
}
catch {
return null;
}
}
};
MixinAuthGuard = __decorate([
__param(1, Optional()),
__param(1, Inject(AuthModuleOptions)),
__param(2, Optional()),
__param(2, Inject(HttpAdapterHost)),
__metadata("design:paramtypes", [Reflector,
AuthModuleOptions,
HttpAdapterHost])
], MixinAuthGuard);
const GuardType = mixin(MixinAuthGuard);
return GuardType;
}
/**
* Exported guard factory using `memoize` from npm
*/
export const AuthGuard = memoize(createAuthGuard);
//# sourceMappingURL=auth.guards.js.map