JWT

JWT 플러그인은 JWT 토큰을 검색하는 엔드포인트와 토큰을 확인하는 JWKS 엔드포인트를 제공합니다.

이 플러그인은 세션을 대체하기 위한 것이 아닙니다. JWT 토큰이 필요한 서비스에 사용하기 위한 것입니다. 인증에 JWT 토큰을 사용하려는 경우 Bearer 플러그인을 확인하세요.

설치

auth 설정에 플러그인 추가

auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [ 
        jwt(), 
    ] 
})

데이터베이스 마이그레이션

마이그레이션을 실행하거나 스키마를 생성하여 필요한 필드와 테이블을 데이터베이스에 추가합니다.

npx @better-auth/cli migrate
npx @better-auth/cli generate

수동으로 필드를 추가하려면 스키마 섹션을 참조하세요.

사용법

플러그인을 설치하면 JWT & JWKS 플러그인을 사용하여 각각의 엔드포인트를 통해 토큰과 JWKS를 가져올 수 있습니다.

JWT

토큰 검색

JWT 토큰을 검색하는 방법에는 여러 가지가 있습니다:

  1. 클라이언트 플러그인 사용 (권장)

auth 클라이언트 구성에 jwtClient 플러그인을 추가합니다:

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { jwtClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
  plugins: [
    jwtClient() 
  ]
})

그런 다음 클라이언트를 사용하여 JWT 토큰을 가져옵니다:

const { data, error } = await authClient.token()
if (error) {
  // 오류 처리
}
if (data) {
  const jwtToken = data.token
  // 외부 서비스에 대한 인증된 요청에 이 토큰을 사용합니다
}

이것은 외부 API 인증을 위해 JWT 토큰이 필요한 클라이언트 애플리케이션에 권장되는 방법입니다.

  1. 세션 토큰 사용

토큰을 가져오려면 /token 엔드포인트를 호출합니다. 다음을 반환합니다:

  {
    "token": "ey..."
  }

auth 설정에 bearer 플러그인이 추가된 경우 요청의 Authorization 헤더에 토큰을 포함해야 합니다.

await fetch("/api/auth/token", {
  headers: {
    "Authorization": `Bearer ${token}`
  },
})
  1. set-auth-jwt 헤더에서

getSession 메서드를 호출하면 set-auth-jwt 헤더에 JWT가 반환되며, 이를 사용하여 서비스에 직접 보낼 수 있습니다.

await authClient.getSession({
  fetchOptions: {
    onSuccess: (ctx)=>{
      const jwt = ctx.response.headers.get("set-auth-jwt")
    }
  }
})

토큰 확인

토큰은 추가 확인 호출이나 데이터베이스 확인 없이 자체 서비스에서 확인할 수 있습니다. 이를 위해 JWKS가 사용됩니다. 공개 키는 /api/auth/jwks 엔드포인트에서 가져올 수 있습니다.

이 키는 자주 변경되지 않으므로 무기한 캐시할 수 있습니다. JWT에 서명하는 데 사용된 키 ID(kid)는 토큰의 헤더에 포함됩니다. 다른 kid를 가진 JWT를 받은 경우 JWKS를 다시 가져오는 것이 좋습니다.

  {
    "keys": [
        {
            "crv": "Ed25519",
            "x": "bDHiLTt7u-VIU7rfmcltcFhaHKLVvWFy-_csKZARUEU",
            "kty": "OKP",
            "kid": "c5c7995d-0037-4553-8aee-b5b620b89b23"
        }
    ]
  }

OAuth 제공자 모드

시스템을 oAuth 규격으로 만드는 경우(OIDC 또는 MCP 플러그인을 사용하는 경우) /token 엔드포인트(oAuth 동등물 /oauth2/token)를 비활성화하고 jwt 헤더 설정(oAuth 동등물 /oauth2/userinfo)을 비활성화해야 합니다.

auth.ts
betterAuth({
  disabledPaths: [
    "/token",
  ],
  plugins: [jwt({
    disableSettingJwtHeader: true,
  })]
})

원격 JWKS와 함께 jose 사용 예제

import { jwtVerify, createRemoteJWKSet } from 'jose'

async function validateToken(token: string) {
  try {
    const JWKS = createRemoteJWKSet(
      new URL('http://localhost:3000/api/auth/jwks')
    )
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // JWT 발급자와 일치해야 하며, BASE_URL입니다
      audience: 'http://localhost:3000', // JWT 대상과 일치해야 하며, 기본적으로 BASE_URL입니다
    })
    return payload
  } catch (error) {
    console.error('Token validation failed:', error)
    throw error
  }
}

// 사용 예제
const token = 'your.jwt.token' // /api/auth/token 엔드포인트에서 가져온 토큰입니다
const payload = await validateToken(token)

로컬 JWKS를 사용한 예제

import { jwtVerify, createLocalJWKSet } from 'jose'


async function validateToken(token: string) {
  try {
    /**
     * 이것은 /api/auth/jwks 엔드포인트에서 가져온 JWKS입니다
     */
    const storedJWKS = {
      keys: [{
        //...
      }]
    };
    const JWKS = createLocalJWKSet({
      keys: storedJWKS.data?.keys!,
    })
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // JWT 발급자와 일치해야 하며, BASE_URL입니다
      audience: 'http://localhost:3000', // JWT 대상과 일치해야 하며, 기본적으로 BASE_URL입니다
    })
    return payload
  } catch (error) {
    console.error('Token validation failed:', error)
    throw error
  }
}

// 사용 예제
const token = 'your.jwt.token' // /api/auth/token 엔드포인트에서 가져온 토큰입니다
const payload = await validateToken(token)

원격 JWKS URL

/jwks 엔드포인트를 비활성화하고 OIDC와 같은 모든 검색에서 이 엔드포인트를 사용합니다.

JWKS가 /jwks에서 관리되지 않거나 jwks가 인증서로 서명되어 CDN에 배치된 경우 유용합니다.

참고: 서명에 사용되는 비대칭 알고리즘을 반드시 지정해야 합니다.

auth.ts
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  }
})

사용자 정의 서명

이것은 고급 기능입니다. 이 플러그인 외부의 구성을 반드시 제공해야 합니다.

구현자:

  • sign 함수를 사용하는 경우 remoteUrl을 정의해야 합니다. 현재 키뿐만 아니라 모든 활성 키를 저장해야 합니다.
  • 로컬 접근 방식을 사용하는 경우 순환 시 서버가 최신 개인 키를 사용하도록 합니다. 배포에 따라 서버를 다시 시작해야 할 수 있습니다.
  • 원격 접근 방식을 사용하는 경우 전송 후 페이로드가 변경되지 않았는지 확인합니다. 사용 가능한 경우 CRC32 또는 SHA256 검사와 같은 무결성 검증을 사용합니다.

로컬 서명

auth.ts
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'EdDSA',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // 이것은 의사 코드입니다
      return await new SignJWT(jwtPayload)
        .setProtectedHeader({
          alg: "EdDSA",
          kid: process.env.currentKid,
          typ: "JWT",
        })
        .sign(process.env.clientPrivateKey);
    },
  },
})

원격 서명

Google KMS, Amazon KMS 또는 Azure Key Vault와 같은 원격 키 관리 서비스를 사용하는 경우 유용합니다.

auth.ts
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // 이것은 의사 코드입니다
      const headers = JSON.stringify({ kid: '123', alg: 'ES256', typ: 'JWT' })
      const payload = JSON.stringify(jwtPayload)
      const encodedHeaders = Buffer.from(headers).toString('base64url')
      const encodedPayload = Buffer.from(payload).toString('base64url')
      const hash = createHash('sha256')
      const data = `${encodedHeaders}.${encodedPayload}`
      hash.update(Buffer.from(data))
      const digest = hash.digest()
      const sig = await remoteSign(digest)
      // integrityCheck(sig)
      const jwt = `${data}.${sig}`
      // verifyJwt(jwt)
      return jwt
    },
  },
})

스키마

JWT 플러그인은 데이터베이스에 다음 테이블을 추가합니다:

JWKS

테이블 이름: jwks

Field NameTypeKeyDescription
idstring각 웹 키의 고유 식별자
publicKeystring-웹 키의 공개 부분
privateKeystring-웹 키의 개인 부분
createdAtDate-웹 키가 생성된 타임스탬프

jwks 테이블의 테이블 이름과 필드를 사용자 정의할 수 있습니다. 플러그인 스키마를 사용자 정의하는 방법에 대한 자세한 내용은 데이터베이스 개념 문서를 참조하세요.

옵션

키 쌍의 알고리즘

키 쌍 생성에 사용되는 알고리즘입니다. 기본값은 Ed25519 곡선을 사용하는 EdDSA입니다. 사용 가능한 옵션은 다음과 같습니다:

auth.ts
jwt({
  jwks: {
    keyPairConfig: {
      alg: "EdDSA",
      crv: "Ed25519"
    }
  }
})

EdDSA

  • 기본 곡선: Ed25519
  • 선택적 속성: crv
    • 사용 가능한 옵션: Ed25519, Ed448
    • 기본값: Ed25519

ES256

  • 추가 속성 없음

RSA256

  • 선택적 속성: modulusLength
    • 숫자를 예상함
    • 기본값: 2048

PS256

  • 선택적 속성: modulusLength
    • 숫자를 예상함
    • 기본값: 2048

ECDH-ES

  • 선택적 속성: crv
    • 사용 가능한 옵션: P-256, P-384, P-521
    • 기본값: P-256

ES512

  • 추가 속성 없음

개인 키 암호화 비활성화

기본적으로 개인 키는 AES256 GCM을 사용하여 암호화됩니다. disablePrivateKeyEncryption 옵션을 true로 설정하여 이를 비활성화할 수 있습니다.

보안상의 이유로 개인 키를 암호화된 상태로 유지하는 것이 좋습니다.

auth.ts
jwt({
  jwks: {
    disablePrivateKeyEncryption: true
  }
})

JWT 페이로드 수정

기본적으로 전체 사용자 객체가 JWT 페이로드에 추가됩니다. definePayload 옵션에 함수를 제공하여 페이로드를 수정할 수 있습니다.

auth.ts
jwt({
  jwt: {
    definePayload: ({user}) => {
      return {
        id: user.id,
        email: user.email,
        role: user.role
      }
    }
  }
})

발급자, 대상, 주체 또는 만료 시간 수정

제공되지 않으면 BASE_URL이 발급자로 사용되고 대상은 BASE_URL로 설정됩니다. 만료 시간은 15분으로 설정됩니다.

auth.ts
jwt({
  jwt: {
    issuer: "https://example.com",
    audience: "https://example.com",
    expirationTime: "1h",
    getSubject: (session) => {
      // 기본적으로 주체는 사용자 ID입니다
      return session.user.email
    }
  }
})