Skip to content

Instantly share code, notes, and snippets.

@Roms1383
Created March 21, 2020 09:41
Show Gist options
  • Save Roms1383/1aa8a5267de0824b497d0b58899f55d6 to your computer and use it in GitHub Desktop.
Save Roms1383/1aa8a5267de0824b497d0b58899f55d6 to your computer and use it in GitHub Desktop.
Medium - Demystifying some of Nest.js with Passport
import { AuthenticatedGuard, Unauthorized } from '@kroms/auth'
import { Controller, Get, UseFilters, UseGuards } from '@nestjs/common'
@Controller()
export class AppController {
@Get('secured-page')
@UseGuards(AuthenticatedGuard)
@UseFilters(Unauthorized) // beware here it requires LocalStrategy explicit export in AuthModule
async securedPage() {
console.log(`@AppController /secured-page`)
return 'secured page'
}
@Get('login-page')
async loginPage() {
console.log(`@AppController /login-page`)
return 'login page'
}
}
import { NestFactory } from '@nestjs/core'
import axios from 'axios'
import * as cookieParser from 'cookie-parser'
import * as cookieSession from 'cookie-session'
import * as passport from 'passport'
import { AppModule } from './app.module'
jest.setTimeout(20000)
const username = '[email protected]'
const password = 'some-password'
const secret = 'some-cookie-secret'
const bootstrap = async () => {
const app = await NestFactory.create(AppModule, { logger: console })
app.use(cookieParser())
app.use(cookieSession({ secret }))
app.use(passport.initialize())
app.use(passport.session())
await app.listen(3000)
return app
}
const teardown = async app => {
await app.close()
app = undefined
}
describe('AuthModule', () => {
let app = undefined
let cookie = undefined
beforeAll(async () => {
app = await bootstrap()
})
afterAll(async () => {
await teardown(app)
})
describe('login', () => {
it('correct credentials', async () => {
try {
const { data, headers, status } = await axios.post(
`http://localhost:3000/login`,
{ username, password },
{ maxRedirects: 0, validateStatus: status => status === 302 },
)
cookie = headers['set-cookie']
.map(cookie => cookie.split(';')[0])
.reduce((acc, cookie) => acc + cookie + ';', '')
expect(data).toBe('Found. Redirecting to /secured-page')
expect(status).toBe(302)
expect(headers['set-cookie']).toBeDefined()
} catch (e) {
console.error(e)
}
})
it('incorrect credentials', async () => {
const { data } = await axios.post(`http://localhost:3000/login`, {
username: 'wrong',
password: 'fake',
})
expect(data).toBe('login page')
})
})
describe('secured page', () => {
it('correctly authenticated', async () => {
const { data } = await axios.get(`http://localhost:3000/secured-page`, {
headers: { cookie: cookie },
})
expect(data).toBe('secured page')
})
it('not authenticated', async () => {
const { data } = await axios.get(`http://localhost:3000/secured-page`)
expect(data).toBe('login page')
})
})
})
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AuthModule } from '@kroms/auth'
@Module({
// or, for example: imports: [AuthModule.register({ successRedirect: '/secured-page', failureRedirect: '/login-page' })],
imports: [AuthModule],
controllers: [AppController],
})
export class AppModule {}
import {
Controller,
Post,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common'
import { Unauthorized } from './auth.filter'
import { LocalStrategy } from './local.strategy'
import { LoginGuard } from './login.guard'
@Controller()
export class AuthController {
constructor(private readonly strategy: LocalStrategy) {}
@Post('login')
@UseGuards(LoginGuard)
@UseFilters(Unauthorized)
async login(@Req() req, @Res() res) {
console.log(`@AuthController /login ${JSON.stringify(req.user)}`)
return req.user
? res.redirect(this.strategy.successRedirect)
: res.redirect(this.strategy.failureRedirect)
}
}
import {
ArgumentsHost,
Catch,
ExceptionFilter,
ForbiddenException,
UnauthorizedException,
} from '@nestjs/common'
import { Response } from 'express'
import { LocalStrategy } from './local.strategy'
@Catch(UnauthorizedException, ForbiddenException)
export class Unauthorized implements ExceptionFilter {
constructor(private readonly strategy: LocalStrategy) {}
catch(
_exception: ForbiddenException | UnauthorizedException,
host: ArgumentsHost,
) {
console.log(
_exception instanceof ForbiddenException ? `@Forbidden` : `@Unauthorized`,
)
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
response.redirect(this.strategy.failureRedirect)
}
}
import { Module, DynamicModule } from '@nestjs/common'
import { PassportModule, IAuthModuleOptions } from '@nestjs/passport'
import { AuthService } from './auth.service'
import { LocalStrategy } from './local.strategy'
import { SessionSerializer } from './session.serializer'
import { AuthController } from './auth.controller'
export interface AuthModuleOptions extends IAuthModuleOptions {
successRedirect: string
failureRedirect: string
}
@Module({
imports: [
PassportModule.register({
session: true,
successRedirect: '/secured-page',
failureRedirect: '/login-page',
}),
],
providers: [AuthService, LocalStrategy, SessionSerializer],
controllers: [AuthController],
exports: [PassportModule, AuthService, LocalStrategy, SessionSerializer], // these exports are required to be able to reuse guard and filter
})
export class AuthModule {
// here we could allow for customizing the service, strategy, serializer
static register({
successRedirect,
failureRedirect,
}: AuthModuleOptions): DynamicModule {
return {
module: AuthModule,
imports: [
PassportModule.register({
session: true,
successRedirect,
failureRedirect,
}),
],
providers: [AuthService, LocalStrategy, SessionSerializer],
controllers: [AuthController],
exports: [PassportModule, AuthService, LocalStrategy, SessionSerializer], // these exports are required to be able to reuse guard and filter
}
}
}
import { Injectable, UnauthorizedException } from '@nestjs/common'
const users = [
{
name: 'john doe',
username: '[email protected]',
password: 'some-password',
role: 'admin',
},
{
name: 'jane doe',
username: '[email protected]',
password: 'some-other-password',
role: 'admin',
},
]
@Injectable()
export class AuthService {
async validateUser(username: string, password: string): Promise<any> {
try {
console.log(
`@AuthService validateUser (username: ${username}, password: ${password})`,
)
if (!username || !password)
throw new UnauthorizedException('Missing username or password')
const { name = undefined, role = undefined } =
users.find(
user => user.username === username && user.password === password,
) || {}
if (!name || !role) return null
return { name, role }
} catch (e) {
return null
}
}
}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
@Injectable()
export class AuthenticatedGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const httpContext = context.switchToHttp()
const request = httpContext.getRequest()
return request.isAuthenticated() && request.user.name && request.user.role
}
}
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { AuthModuleOptions, PassportStrategy } from '@nestjs/passport'
import { Strategy } from 'passport-local'
import { AuthService } from './auth.service'
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly service: AuthService,
private readonly options: AuthModuleOptions,
) {
super()
}
async validate(username: string, password: string): Promise<any> {
console.log(`@LocalStrategy validate`)
const user = await this.service.validateUser(username, password)
console.log(`@LocalStrategy validate ${JSON.stringify(user)}`)
if (!user) throw new UnauthorizedException('Unknown user')
return user
}
public successRedirect: string = this.options['successRedirect'] // '/secured-page'
public failureRedirect: string = this.options['failureRedirect'] // '/login-page'
}
import { Injectable, ExecutionContext } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class LoginGuard extends AuthGuard('local') {
async canActivate(context: ExecutionContext) {
const result = (await super.canActivate(context)) as boolean
const request = context.switchToHttp().getRequest()
await super.logIn(request)
return result
}
}
import { PassportSerializer } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
@Injectable()
export class SessionSerializer extends PassportSerializer {
serializeUser(user: any, done: (err: any, id?: any) => void): void {
console.log(`@SessionSerializer serializeUser`)
done(null, user)
}
deserializeUser(payload: any, done: (err: any, id?: any) => void): void {
console.log(`@SessionSerializer deserializeUser`)
done(null, payload)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment