Workaround for `expires_in` Type Error in Auth.js (formerly NextAuth.js)

May 6, 2024 (2mo ago)

261 views

Auth.js (๊ตฌ NextAuth.js) ์‚ฌ์šฉ ์‹œ ๋ฐœ์ƒํ•˜๋Š” `expires_in` ํƒ€์ž… ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.
Intro

TLDR

์ œ forked ๋ฒ„์ „์˜ oauth4webapi๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๋ฉด, `expires_in`์ด string ํƒ€์ž…์ด์–ด๋„ ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค. forked ๋ฒ„์ „์€ @jacobkim/oauth4webapi์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์Šˆ ์„ค๋ช…

๋„ค์ด๋ฒ„์™€ Azure AD B2C ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์‚ฌ์šฉํ•˜๋ ค ํ–ˆ์„๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

[auth][cause]: OperationProcessingError: "response" body "expires_in" property must be a positive number

์•Œ๊ณ ๋ณด๋‹ˆ ๋„ค์ด๋ฒ„ ์ธก์—์„œ OAuth 2.0 ์ŠคํŽ™์„ ์ค€์ˆ˜ํ•˜์ง€ ์•Š๊ณ , expires_in๋ณ€์ˆ˜๋ฅผ number ํƒ€์ž…์ด ์•„๋‹Œ, string ์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

Auth.js๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ oauth4webapi ์ด๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ OAuth 2.0 ์ธ์ฆ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” OAuth 2.0 ์ŠคํŽ™์„ ์—„๊ฒฉํžˆ ์ง€ํ‚ค๊ธฐ์—, expires_in์„ ์ž๋™์œผ๋กœ number type์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ค€๋‹ค๊ฑฐ๋‚˜ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ๋ช‡๋ฒˆ ๋‹ค๋ฅธ ๋ถ„๋“ค์ด expires_in์„ ์ž๋™์œผ๋กœ number ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” PR์„ ์˜ฌ๋ ธ์ง€๋งŒ, ๋ชจ๋‘ ๊ฑฐ์ ˆ๋‹นํ–ˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” OAuth 2.0 ์ŠคํŽ™์„ ์ฒ ์ €ํžˆ ์ค€์ˆ˜ํ•˜๋Š” oauth4webapi ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐœ๋ฐœ์ž์˜ ์˜๊ฒฌ์„ ์™„์ „ํžˆ ์กด์ค‘ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋„ค์ด๋ฒ„ ์ธก ์—์„œ๋„ ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๋•Œ๋ฌธ์— expires_in์„ ๋„˜๋ฒ„ ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•  ์ƒ๊ฐ์ด ์—†์–ด ๋ณด์˜€์Šต๋‹ˆ๋‹ค.

๋„ค์ด๋ฒ„ ํฌ๋Ÿผ ์Šคํฌ๋ฆฐ์ƒท
๋„ค์ด๋ฒ„ ๊ฐœ๋ฐœ์ž ํฌ๋Ÿผ์— ์˜ฌ๋ผ์˜จ OAuth 2.0 ์ŠคํŽ™ ๊ด€๋ จ ๋ฌธ์˜

ํ•ด๊ฒฐ๋ฐฉ์•ˆ

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹ ์—๊ฒŒ ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ์•ˆ #1 (์ถ”์ฒœ)

๋งŒ์•ฝ ์ œ forked ๋ฒ„์ „์˜ oauth4webapi๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๊ดœ์ฐฎ๋‹ค๋ฉด, ์•„๋ž˜ npm ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ œ forked ๋ฒ„์ „์€ `expires_in`์ด string ํƒ€์ž…์ด์–ด๋„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, forked ๋ ˆํฌ๋Š” @jacobhjkim/oauth4webapi์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

NPM์˜ overrides ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด, oauth4webapi ํŒจํ‚ค์ง€๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// package.json
  ...
            
  "overrides": {
    "oauth4webapi": "npm:@jacobkim/oauth4webapi@^2.10.4"
  }

๋งŒ์•ฝ ์ €์ฒ˜๋Ÿผ pnpm์„ ์‚ฌ์šฉํ•˜์‹ ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด overrides๊ฐ€ ์•„๋‹Œ pnpm.overrides๋ฅผ ์‚ฌ์šฉํ•˜์…”์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์•ˆ๊ทธ๋Ÿฌ๋ฉด ์ €์ฒ˜๋Ÿผ ํ•œ์‹œ๊ฐ„๋™์•ˆ pnpm ๊นƒํ—™ ๋ ˆํฌ์—์„œ ์ด์Šˆ๋“ค์„ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์žˆ์„๊ฒ๋‹ˆ๋‹ค.)

// package.json
  ...
            
  "pnpm": {
    "overrides": {
      "oauth4webapi": "npm:@jacobkim/oauth4webapi@^2.10.4"
    }
  }

npm์€ overrides๊ฐ€ ์ž˜ ์•ˆ๋  ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

npm CLI๋Š” overrides ๊ธฐ๋Šฅ์ด ์ž˜ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ œ๋ณด๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ด€๋ จ ๊นƒํ—™ ์ด์Šˆ๋“ค ์ž…๋‹ˆ๋‹ค:

๊ทธ๋Ÿฌ๋‹ˆ ์•„์ง๋„ npm์„ ์‚ฌ์šฉํ•˜๊ณ  ๊ณ„์‹œ๋‹ค๋ฉด, pnpm์„ ์‚ฌ์šฉํ•˜์‹œ๋Š”๊ฑธ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. pnpm์ด ๋‹ค๋ฐฉ๋ฉด์—์„œ ํ›จ์”ฌ ์ข‹์Šต๋‹ˆ๋‹ค.


ํ•ด๊ฒฐ๋ฐฉ์•ˆ #2

๋งŒ์•ฝ ๋‹น์‹ ์ด ํŒจํ‚ค์ง€ ์˜ค๋ฒ„๋ผ์ด๋”ฉ์„ ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์œผ์‹œ๋‹ค๊ฑฐ๋‚˜, ์ œ fork๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๊ป˜๋ฆ„์น™ํ•˜์‹œ๋‹ค๋ฉด ๋‹ค๋ฅธ ํ•ด๊ฒฐ๋ฐฉ์•ˆ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ @numman-ali๋‹˜์ด ์ œ์•ˆํ•˜๋“ฏ, ์ปค์Šคํ…€ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์ž‘์„ฑํ• ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํŠน์ • provider๋Š” ์ด ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ OAuth request๋ฅผ ํ•ธ๋“ค๋ง ํ•˜๋Š”๊ฑฐ์ฃ .

// app/api/auth/[...nextauth]/route.ts

import { type NextRequest, NextResponse } from "next/server";

import {
  auth,
  GET as AuthGET,
  instagramFetchInterceptor,
  POST as AuthPOST,
} from "@/auth"

const originalFetch = fetch

export async function POST(req: NextRequest) {
  return await AuthPOST(req)
}

export async function GET(req: NextRequest) {
  const url = new URL(req.url)

  if (url.pathname === "/api/auth/callback/naver") {
    const session = await auth()
    if (!session?.user) {
      /* Prevent user creation for instagram access token */
      const signInUrl = new URL("/?modal=sign-in", req.url)
      return NextResponse.redirect(signInUrl)
    }

     /* Intercept the fetch request to patch access_token request to be oauth compliant */
    global.fetch = naverFetchInterceptor(originalFetch)  <- your custom interceptor
    const response = await AuthGET(req)
    global.fetch = originalFetch
    return response
  }

  return await AuthGET(req)
}

๊ฐœ์ธ์ ์œผ๋กœ๋Š” overrides ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด, ์ธํ„ฐ์…‰ํ„ฐ ๋ฐฉ์‹๋ณด๋‹ค ์‰ฝ๊ณ  ๋œ ๋จธ๋ฆฌ๊ฐ€ ์•„ํ”Œ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.