@codecanva/nest-auth
v0.3.0
Published
Reusable NestJS authentication module with JWT access + refresh-token rotation, multi-device sessions, and pluggable persistence.
Maintainers
Readme
@codecanva/nest-auth
Reusable NestJS authentication module. JWT access + refresh-token rotation, multi-device sessions, replay detection, Login with Google, and pluggable persistence (no DB lock-in).
Features
- Short-lived access JWT + long-lived refresh JWT
- Refresh-token rotation on every refresh (with replay detection →
revokeAllForUser) - Multi-device sessions (one row per session, hashed at rest)
- Login with Google — verify a Google ID token (frontend flow) or an OAuth redirect
code(server flow), then issue your own JWTs - Pluggable
RefreshTokenStoreandUserValidator— bring your own DB @CurrentUser(),@Public()decoratorsJwtAuthGuard(honours@Public()) andRefreshAuthGuard- Configurable issuer, audience, clock tolerance, optional hash pepper
Install
npm install @codecanva/nest-auth \
@nestjs/jwt @nestjs/passport passport passport-jwt \
class-validator class-transformerQuick install (CLI)
Scaffold everything into an existing NestJS project — user/auth files, env vars,
AppModule + main.ts wiring, and dependencies — in one command:
# Mongoose persistence (default): generates user + refresh-token modules
npx @codecanva/nest-auth init
# In-memory store, no DB — great for a quick trial
npx @codecanva/nest-auth init --store memory
# Preview without writing anything
npx @codecanva/nest-auth init --dry-runThe installer generates an AuthIntegrationModule (a single drop-in module that
bundles the store, validator, controller, and the global JwtAuthGuard), adds it
to your AppModule, and enables a global ValidationPipe. Existing files are
never overwritten without --force, and every edited file is backed up to
<file>.bak.
| Flag | Effect |
| --- | --- |
| --store <mongoose\|memory> | Persistence to scaffold (default mongoose) |
| --dir <path> | Target project root (default: current directory) |
| --no-wire | Don't touch app.module.ts / main.ts |
| --skip-install | Don't install npm dependencies |
| --force | Overwrite files that already exist |
| --dry-run | Show planned changes, write nothing |
After running it, set real values for JWT_ACCESS_SECRET, JWT_REFRESH_SECRET,
and TOKEN_HASH_PEPPER in .env (and, for --store mongoose, make sure a
MongooseModule.forRoot(...) connection exists at the app root). For a fully
manual, step-by-step walkthrough see INSTALLATION.md.
Usage
1. Implement the two interfaces against your data layer
// users/user.validator.ts
import { Injectable } from '@nestjs/common';
import { AuthUser, UserValidator } from '@codecanva/nest-auth';
@Injectable()
export class MyUserValidator implements UserValidator {
async validateCredentials(email: string, password: string): Promise<AuthUser | null> {
// load user, bcrypt.compare, return { id, email, roles } or null
}
async findById(userId: string | number): Promise<AuthUser | null> {
// load by id, return AuthUser or null
}
}// auth/refresh-token.store.ts — example with TypeORM
import { Injectable } from '@nestjs/common';
import {
CreateRefreshTokenInput,
RefreshTokenStore,
StoredRefreshToken,
} from '@codecanva/nest-auth';
@Injectable()
export class MyRefreshTokenStore implements RefreshTokenStore {
// create / findById / consume / revokeById / revokeAllForUser
// IMPORTANT: `consume` must be atomic (single UPDATE ... WHERE revoked_at IS NULL
// AND token_hash = $hash RETURNING *). Non-atomic impls split sessions under load.
}2. Register the module
import { AuthModule } from '@codecanva/nest-auth';
@Module({
imports: [
AuthModule.forRootAsync({
imports: [ConfigModule],
useFactory: (cfg: ConfigService) => ({
accessSecret: cfg.getOrThrow('JWT_ACCESS_SECRET'),
refreshSecret: cfg.getOrThrow('JWT_REFRESH_SECRET'),
accessTtl: '15m',
refreshTtl: '30d',
tokenHashPepper: cfg.get('TOKEN_HASH_PEPPER'),
issuer: 'my-app',
}),
inject: [ConfigService],
store: { useClass: MyRefreshTokenStore },
validator: { useClass: MyUserValidator },
}),
],
})
export class AppModule {}3. Apply the guard globally (optional)
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '@codecanva/nest-auth';
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],4. Use in controllers
import {
AuthService, CurrentUser, LoginDto, Public, RefreshTokenDto,
} from '@codecanva/nest-auth';
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Public() @Post('login')
login(@Body() dto: LoginDto) { return this.auth.login(dto.email, dto.password); }
@Public() @Post('refresh')
refresh(@Body() dto: RefreshTokenDto) { return this.auth.refresh(dto.refreshToken); }
@Post('logout')
logout(@Body() dto: RefreshTokenDto) { return this.auth.logout(dto.refreshToken); }
}
@Controller('me')
export class MeController {
@Get() me(@CurrentUser() user: AuthUser) { return user; }
}Login with Google
Google login reuses the same pluggable design: the library verifies the Google token and issues your JWTs; you decide how a Google identity maps to a user. Both the frontend (ID-token) and server-side (redirect) flows are supported.
1. Install the verifier dependency
Verification uses Google's official library (an optional peer dependency — only needed if you enable Google login):
npm install google-auth-library2. Enable it in the module config
Add a google block to the options returned by useFactory:
AuthModule.forRootAsync({
useFactory: (cfg: ConfigService) => ({
accessSecret: cfg.getOrThrow('JWT_ACCESS_SECRET'),
refreshSecret: cfg.getOrThrow('JWT_REFRESH_SECRET'),
google: {
clientId: cfg.getOrThrow('GOOGLE_CLIENT_ID'),
// Only needed for the server-side redirect flow:
clientSecret: cfg.get('GOOGLE_CLIENT_SECRET'),
redirectUri: cfg.get('GOOGLE_REDIRECT_URI'),
// Optional hardening:
// allowedHostedDomains: ['your-company.com'], // restrict to a Workspace domain
// requireEmailVerified: true, // default true
},
}),
inject: [ConfigService],
store: { useClass: MyRefreshTokenStore },
validator: { useClass: MyUserValidator },
})clientId may be a single id or an array (e.g. to accept tokens from web + iOS
- Android clients). To swap Google's verifier for your own, pass
googleClient: { useClass: MyGoogleAuthClient }(implement theGoogleAuthClientinterface).
3. Implement validateGoogleUser on your UserValidator
Called only after the token is cryptographically verified. Typically a
find-or-create — return the AuthUser, or null to reject the login:
async validateGoogleUser(profile: GoogleProfile): Promise<AuthUser | null> {
// profile = { googleId, email, emailVerified, name, picture, hostedDomain, raw }
const user = await this.users.findOrCreateFromGoogle({
googleId: profile.googleId,
email: profile.email,
});
return { id: user.id, email: user.email, roles: user.roles };
}4. Add the endpoints
import { AuthService, GoogleLoginDto, GoogleCodeDto, Public } from '@codecanva/nest-auth';
import type { Response } from 'express';
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
// Frontend flow: the browser/app sends a Google ID token.
@Public() @Post('google') @HttpCode(200)
google(@Body() dto: GoogleLoginDto) {
return this.auth.loginWithGoogle(dto.idToken); // → { accessToken, refreshToken, user }
}
// Server-side redirect flow (needs clientSecret + redirectUri):
@Public() @Get('google')
async redirect(@Res() res: Response) {
res.redirect(await this.auth.getGoogleAuthUrl({ prompt: 'select_account' }));
}
@Public() @Get('google/callback')
callback(@Query() q: GoogleCodeDto) {
return this.auth.loginWithGoogleCode(q.code);
}
}5. Frontend (ID-token flow)
Use Google Identity Services
to obtain an ID token, then POST it to /auth/google:
<script src="https://accounts.google.com/gsi/client" async></script>
<div id="g_id_onload"
data-client_id="YOUR_GOOGLE_CLIENT_ID"
data-callback="onGoogle"></div>
<div class="g_id_signin" data-type="standard"></div>
<script>
async function onGoogle(response) {
const res = await fetch('/auth/google', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ idToken: response.credential }),
});
const { accessToken, refreshToken, user } = await res.json();
// store tokens, then use accessToken as a Bearer token on API calls
}
</script>Failure responses: invalid/expired token → 401; verified but not
permitted (unverified email, disallowed hosted domain, or validateGoogleUser
returned null) → 403; Google login not configured → 501.
API surface
AuthModule.forRootAsync(opts)— only entry pointAuthService—login/refresh/logout/logoutAll/loginWithGoogle/getGoogleAuthUrl/loginWithGoogleCode- Guards —
JwtAuthGuard,RefreshAuthGuard - Decorators —
@Public(),@CurrentUser() - DTOs —
LoginDto,RefreshTokenDto,GoogleLoginDto,GoogleCodeDto - Errors —
AuthError,InvalidCredentialsError,InvalidRefreshTokenError,RefreshTokenExpiredError,RefreshTokenReuseDetectedError,UserNotFoundError,GoogleAuthError,InvalidGoogleTokenError,GoogleAccountNotAllowedError,GoogleAuthNotConfiguredError - Interfaces —
AuthModuleOptions,AuthModuleAsyncOptions,AuthUser,JwtPayload,RefreshTokenStore,StoredRefreshToken,CreateRefreshTokenInput,SessionMetadata,UserValidator,GoogleAuthOptions,GoogleProfile,GoogleAuthClient,GoogleAuthUrlOptions
Local development / publishing
npm install # install deps
npm run start:dev # run the demo app at :3000 (uses in-memory store + validator)
npm run build:lib # compile lib → dist/
npm publish # publish (runs build:lib via prepublishOnly)To consume from a sibling project before publishing:
# in this repo
npm run build:lib && npm pack
# in the consumer
npm install /path/to/codecanva-nest-auth-0.1.0.tgzDemo endpoints (when running start:dev)
POST /auth/login { "email": "[email protected]", "password": "password123" }
POST /auth/refresh { "refreshToken": "..." }
POST /auth/logout { "refreshToken": "..." } # 204
POST /auth/google { "idToken": "..." } # set GOOGLE_CLIENT_ID to enable
GET /auth/google # 302 → Google consent (redirect flow)
GET /auth/google/callback?code=... # redirect-flow callback
GET /me # bearer access token requiredSecurity notes
- Refresh tokens are JWTs; the hash of the JWT (sha256 + optional pepper) is stored, never the raw token.
consumemust be atomic — non-atomic impls split sessions under concurrent refresh.- On replay (token hash mismatch or already-revoked row), every active session for that user is revoked.
- Always serve auth over HTTPS. Store the refresh token in an httpOnly cookie when possible.
