Auth.js is a very convenient authentication library for integrating OAuth. It was initially designed for Next.js. Now the official team provides integrations for some commonly used frameworks, but unfortunately, there is no official support for Nest.js.
This article will construct an AuthModule suitable for Nest.js from scratch. Let's get started.
Preparation#
We need to use the underlying dependency of @auth/core
and wrap it.
npm i @auth/core
Next, we need to understand how an Auth adapts and integrates with a Framework. The official support for Express is provided, and we can learn the integration steps from the source code.
https://github.com/nextauthjs/next-auth/blob/main/packages/frameworks-express/src/index.ts
From the source code, we learn that @auth/core
is an abstraction layer, and we need to do two things: first, convert the original framework's Request to WebRequest, and then let AuthCore handle it; second, convert the WebResponse processed by AuthCore back to the original framework's Response.
Now that we understand the principle, we can proceed.
Writing a Converter#
First, we create an auth
module, for example, src/modules/auth.module.ts
. The module content is initially empty. Then we write a Request/Response converter needed by AuthCore
. Here we create it as src/module/auth/auth.implement.ts
.
export function CreateAuth(config: ServerAuthConfig) {
return async (req: IncomingMessage, res: ServerResponse) => {
try {
setEnvDefaults(process.env, config)
const auth = await Auth(await toWebRequest(req), config)
await toServerResponse(req, auth, res)
} catch (error) {
console.error(error)
// throw error
res.end(error.message)
}
}
}
async function toWebRequest(req: IncomingMessage) {
const host = req.headers.host || 'localhost'
const protocol = req.headers['x-forwarded-proto'] || 'http'
const base = ${protocol}://${host}
return getRequest(base, req)
}
async function toServerResponse(
req: IncomingMessage,
response: Response,
res: ServerResponse,
) {
response.headers.forEach((value, key) => {
if (value) {
res.setHeader(key, value)
}
})
res.setHeader('Content-Type', response.headers.get('content-type') || '')
res.setHeader('access-control-allow-methods', 'GET, POST')
res.setHeader('access-control-allow-headers', 'content-type')
res.setHeader(
'access-control-allow-origin',
req.headers.origin || req.headers.referer || req.headers.host || '*',
)
res.setHeader('access-control-allow-credentials', 'true')
const text = await response.text()
res.writeHead(response.status, response.statusText)
res.end(text)
}
</Tab>
<Tab label="getRequest">
```ts filename="src/module/auth/req.transformer.ts"
import { PayloadTooLargeException } from '@nestjs/common'
import type { IncomingMessage } from 'node:http'
/**
* @param {import('http').IncomingMessage} req
*/
function get_raw_body(req) {
const h = req.headers
if (!h['content-type']) {
return null
}
const content_length = Number(h['content-length'])
// check if no request body
if (
(req.httpVersionMajor === 1 &&
isNaN(content_length) &&
h['transfer-encoding'] == null) ||
content_length === 0
) {
return null
}
if (req.destroyed) {
const readable = new ReadableStream()
readable.cancel()
return readable
}
let size = 0
let cancelled = false
return new ReadableStream({
start(controller) {
req.on('error', (error) => {
cancelled = true
controller.error(error)
})
req.on('end', () => {
if (cancelled) return
controller.close()
})
req.on('data', (chunk) => {
if (cancelled) return
size += chunk.length
if (size > content_length) {
cancelled = true
const constraint = content_length
? 'content-length'
: 'BODY_SIZE_LIMIT'
const message = `request body size exceeded ${constraint} of ${content_length}`
const error = new PayloadTooLargeException(message)
controller.error(error)
return
}
controller.enqueue(chunk)
if (controller.desiredSize === null || controller.desiredSize <= 0) {
req.pause()
}
})
},
pull() {
req.resume()
},
cancel(reason) {
cancelled = true
req.destroy(reason)
},
})
}
export async function getRequest(
base: string,
req: IncomingMessage,
): Promise<Request> {
const headers = req.headers as Record<string, string>
// @ts-expect-error
const request = new Request(base + req.originalUrl, {
method: req.method,
headers,
body: get_raw_body(req),
credentials: 'include',
// @ts-expect-error
duplex: 'half',
})
return request
}
Intercept Requests for Auth.js Processing#
In summary, we have now implemented the adaptation of AuthCore, and next, we need to pass the request to AuthCore for processing.
// Create an auth handler
const authHandler = CreateAuth(config) // your auth config
In Nest.js, there are two ways to capture routes. We can use a regex match in the Controller for a generic path and then let authHandler handle it, or use middleware.
The Controller is as follows.
@Controller('auth')
export class AuthController {
@Get('/*')
@Post('/*')
async handle(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
return authHandler(req, res)
}
}
Here we use Middleware because the priority of middleware is higher than anything else.
Write a Middleware.
export class AuthMiddleware implements NestMiddleware {
async use(req: IncomingMessage, res: ServerResponse, next: () => void) {
if (req.method !== 'GET' && req.method !== 'POST') {
next()
return
}
await authHandler(req, res)
next()
}
}
Use this Middleware in AuthModule.
@Module({})
export class AuthModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes(`/auth/(.*)`)
}
}
Now, all /auth/*
will be handled by authHandler. At this point, it can already be used. For example, the following authConfig.
export const authConfig: ServerAuthConfig = {
basePath: isDev ? '/auth' : `/api/v${API_VERSION}/auth`,
secret: AUTH.secret,
callbacks: {
redirect({ url }) {
return url
},
},
providers: [
GitHub({
clientId: AUTH.github.clientId,
clientSecret: AUTH.github.clientSecret,
}),
],
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
authenticatorsTable: authenticators,
}),
}
This allows for GitHub OAuth login and recording User information in the Database.
User Session#
After logging in, we need to obtain the Session to determine the login status.
Write a Service.
export interface SessionUser {
sessionToken: string
userId: string
expires: string
}
@Injectable()
export class AuthService {
private async getSessionBase(req: IncomingMessage, config: ServerAuthConfig) {
setEnvDefaults(process.env, config)
const protocol = (req.headers['x-forwarded-proto'] || 'http') as string
const url = createActionURL(
'session',
protocol,
// @ts-expect-error
new Headers(req.headers),
process.env,
config.basePath,
)
const response = await Auth(
new Request(url, { headers: { cookie: req.headers.cookie ?? '' } }),
config,
)
const { status = 200 } = response
const data = await response.json()
if (!data || !Object.keys(data).length) return null
if (status === 200) return data
}
getSessionUser(req: IncomingMessage) {
return new Promise<SessionUser | null>((resolve) => {
this.getSessionBase(req, {
...authConfig,
callbacks: {
...authConfig.callbacks,
async session(...args) {
resolve(args[0].session as SessionUser)
const session =
(await authConfig.callbacks?.session?.(...args)) ??
args[0].session
const user = args[0].user ?? args[0].token
return { user, ...session } satisfies Session
},
},
}).then((session) => {
if (!session) {
resolve(null)
}
})
})
}
}
Modularization#
Next, we can make the above code more compliant with Nest module specifications.
We create a DynamicModule for configuring AuthModule.
```ts filename="auth.module.ts" const AuthConfigInjectKey = Symbol()@Module({})
@Global()
export class AuthModule implements NestModule {
constructor(
@Inject(AuthConfigInjectKey) private readonly config: ServerAuthConfig,
) {}
static forRoot(config: ServerAuthConfig): DynamicModule {
return {
module: AuthModule,
global: true,
exports: [AuthService],
providers: [
{
provide: AuthService,
useFactory() {
return new AuthService(config)
},
},
{
provide: AuthConfigInjectKey,
useValue: config,
},
],
}
}
configure(consumer: MiddlewareConsumer) {
const config = this.config
consumer
.apply(AuthMiddleware)
.forRoutes(`${config.basePath || '/auth'}/(.*)`)
}
}
</Tab>
<Tab label="Service">
```ts filename="auth.service.ts" {19}
import { Injectable } from '@nestjs/common'
import { ServerAuthConfig } from './auth.implement'
import {
Auth,
createActionURL,
setEnvDefaults,
type Session,
} from '@meta-muse/complied'
import { IncomingMessage } from 'http'
export interface SessionUser {
sessionToken: string
userId: string
expires: string
}
@Injectable()
export class AuthService {
constructor(private readonly authConfig: ServerAuthConfig) {}
private async getSessionBase(req: IncomingMessage, config: ServerAuthConfig) {
setEnvDefaults(process.env, config)
const protocol = (req.headers['x-forwarded-proto'] || 'http') as string
const url = createActionURL(
'session',
protocol,
// @ts-expect-error
new Headers(req.headers),
process.env,
config.basePath,
)
const response = await Auth(
new Request(url, { headers: { cookie: req.headers.cookie ?? '' } }),
config,
)
const { status = 200 } = response
const data = await response.json()
if (!data || !Object.keys(data).length) return null
if (status === 200) return data
}
getSessionUser(req: IncomingMessage) {
const { authConfig } = this
return new Promise<SessionUser | null>((resolve) => {
this.getSessionBase(req, {
...authConfig,
callbacks: {
...authConfig.callbacks,
async session(...args) {
resolve(args[0].session as SessionUser)
const session =
(await authConfig.callbacks?.session?.(...args)) ??
args[0].session
const user = args[0].user ?? args[0].token
return { user, ...session } satisfies Session
},
},
}).then((session) => {
if (!session) {
resolve(null)
}
})
})
}
}
Now, /auth
is also configurable in Middleware.
Register in AppModule:
@Module({
imports: [
AuthModule.forRoot(authConfig),
],
controllers: [],
providers: []
})
export class AppModule {}
Authentication Guard#
We need to write a guard that only allows access to certain routes after successful login verification.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Inject(AuthService)
private readonly authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<any> {
const req = context.switchToHttp().getRequest()
const session = await this.authService.getSessionUser(req.raw)
req.raw['session'] = session
req.raw['isAuthenticated'] = !!session
if (!session) {
throw new UnauthorizedException()
}
return !!session
}
}
At the same time, we will attach session
and isAuthenticated
to the original request.
Great job.
Example#
Finally, here is the open-source template mentioned above.
Or if you still feel confused after reading the above content, no worries, you can directly use the template or draw inspiration from it.
This article is updated by Mix Space to xLog. The original link is https://innei.in/posts/tech/use-auth-js-in-nestjs