Auth.js は、OAuth を非常に便利に接続できる認証ライブラリです。最初は Next.js のために設計されました。現在、公式は一般的なフレームワークの統合を提供していますが、不幸なことに、Nest.js の公式サポートはありません。
この記事では、ゼロから Nest.js に適した AuthModule を構築します。それでは始めましょう。
準備#
私たちは @auth/core
の基盤依存関係を使用し、それを基にラッピングします。
npm i @auth/core
次に、Auth がどのようにフレームワークに適合し接続されるかを理解する必要があります。公式は Express のサポートを提供しており、ソースコードから接続の手順を学ぶことができます。
https://github.com/nextauthjs/next-auth/blob/main/packages/frameworks-express/src/index.ts
ソースコードから、@auth/core
は抽象層であり、私たちは二つのことを行う必要があります。第一に、元のフレームワークのリクエストを WebRequest に変換し、次に AuthCore が処理します。第二に、AuthCore が処理を終えた後の WebResponse を元のフレームワークのレスポンスに変換します。
原理が分かったので、次は簡単です。
変換器の作成#
まず、auth
モジュールを作成します。例えば src/modules/auth.module.ts
です。モジュールの内容は空のままにします。そして、AuthCore
が必要とするリクエスト / レスポンスの変換器を作成します。ここでは 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
}
リクエストを Auth.js に処理させる#
以上のように、現在私たちは AuthCore の適合を実現しました。次に、リクエストを AuthCore に処理させる必要があります。
// Create a auth handler
const authHandler = CreateAuth(config) // your auth config
Nest.js では、ルートをキャッチするために二つの方法があります。Controller の正規表現を使用して汎用パスをマッチさせ、authHandler に処理させることができます。または、ミドルウェアを使用することもできます。
Controller は以下のようになります。
@Controller('auth')
export class AuthController {
@Get('/*')
@Post('/*')
async handle(@Req() req: FastifyRequest, @Res() res: FastifyReply) {
return authHandler(req, res)
}
}
ここでは、ミドルウェアを使用します。なぜなら、ミドルウェアの優先度がすべてに優先するからです。
ミドルウェアを作成します。
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()
}
}
AuthModule でこのミドルウェアを使用します。
@Module({})
export class AuthModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes(`/auth/(.*)`)
}
}
これで、すべての /auth/*
が authHandler に引き継がれます。ここまでで、すでに使用可能です。例えば、以下の 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,
}),
}
これで GitHub の OAuth ログインとユーザー情報をデータベースに記録することができます。
ユーザーセッション#
ログインが完了したら、セッションを取得してログイン状態を判断する必要があります。
サービスを作成します。
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)
}
})
})
}
}
モジュール化#
次に、上記のコードを Nest モジュールの規範により適合させることができます。
AuthModule を構成するための DynamicModule を作成します。
```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)
}
})
})
}
}
これで、/auth
はミドルウェアの中でも設定可能になりました。
AppModule に登録します:
@Module({
imports: [
AuthModule.forRoot(authConfig),
],
controllers: [],
providers: []
})
export class AppModule {}
認証ガード#
ログイン認証が成功した後にのみ、特定のルートにアクセスできるようにするガードを作成する必要があります。
@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
}
}
同時に、session
isAuthenticated
を元のリクエストに追加します。
これで大功告成です。
例#
最後に、私のオープンソースの上記テンプレートを提供します。
または、上記の内容を見てもまだ分からない場合は、テンプレートを直接使用するか、テンプレートからインスピレーションを得ることができます。
この記事は Mix Space によって xLog に同期更新されました。原始リンクは https://innei.in/posts/tech/use-auth-js-in-nestjs