Nodemailer Provider
개요
Nodemailer 프로바이더는 이메일을 통해 “매직 링크”를 보냅니다. 이 링크에는 검증 토큰이 포함된 URL이 있어, 사용자가 로그인할 수 있게 해줍니다.
하나 이상의 OAuth 서비스와 함께 이메일을 통한 로그인 기능을 추가하면, 사용자가 OAuth 계정에 접근할 수 없을 때(예: 계정이 잠기거나 삭제된 경우) 대체 로그인 방법을 제공할 수 있습니다.
Nodemailer 프로바이더는 하나 이상의 OAuth 프로바이더와 함께 사용하거나, 대신 사용할 수 있습니다.
작동 방식
처음 로그인할 때, 제공된 이메일 주소로 **인증 토큰(Verification Token)**이 전송됩니다. 기본적으로 이 토큰은 24시간 동안 유효합니다. 이 시간 내에 인증 토큰을 사용하면(예: 이메일의 링크를 클릭하면) 사용자 계정이 생성되고 로그인됩니다.
누군가가 로그인할 때 _기존 계정_의 이메일 주소를 제공하면, 이메일이 전송되고 이메일의 링크를 따라가면 해당 이메일 주소와 연결된 계정으로 로그인됩니다.
Nodemailer 프로바이더는 JSON Web Token과 데이터베이스 관리 세션 모두와 함께 사용할 수 있지만, 데이터베이스를 구성해야 합니다. 데이터베이스를 사용하지 않고 이메일 로그인을 활성화하는 것은 불가능합니다.
설정
- Auth.js는
nodemailer
를 의존성으로 포함하지 않습니다. Nodemailer 프로바이더를 사용하려면 직접 설치해야 합니다.
npm install nodemailer
-
SMTP 계정이 필요합니다. 가능하면
nodemailer
와 호환되는 서비스 중 하나를 사용하는 것이 좋습니다. Nodemailer는 다른 전송 방식과도 작동하지만, HTTP 기반 이메일 서비스를 사용하려면 Resend나 Sendgrid와 같은 다른 Auth.js 프로바이더를 사용하는 것을 권장합니다. -
SMTP 서버 연결을 설정하는 방법은 두 가지입니다.
연결 문자열을 사용하거나 nodemailer
설정 객체를 사용할 수 있습니다.
EMAIL_SERVER=smtp://username:password@smtp.example.com:587
EMAIL_FROM=noreply@example.com
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: ...,
providers: [
Nodemailer({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
})
-
이메일 인증 토큰을 저장하기 위해 데이터베이스 어댑터 중 하나를 설정하는 것을 잊지 마세요.
-
이제
/api/auth/signin
에서 이메일 주소로 로그인 프로세스를 시작할 수 있습니다.
사용자가 이메일 주소를 처음으로 인증할 때까지는 사용자 계정(즉, Users
테이블의 항목)이 생성되지 않습니다. 이미 계정과 연결된 이메일 주소인 경우, 사용자가 이메일의 링크를 사용하면 해당 계정으로 로그인됩니다.
Customization
이메일 본문
Nodemailer()
에 sendVerificationRequest
옵션으로 커스텀 함수를 전달하여 전송되는 로그인 이메일을 완전히 커스터마이징할 수 있습니다.
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest({
identifier: email,
url,
provider: { server, from },
}) {
// 여기에 함수를 작성
},
}),
],
})
예를 들어, 다음은 내장된 sendVerificationRequest()
메서드의 소스 코드입니다. 이 메서드에서 HTML을 렌더링(html()
)하고 이메일 제공자에게 네트워크 요청(transport.sendMail()
)을 보내 실제로 이메일을 전송하는 것을 확인할 수 있습니다.
import { createTransport } from "nodemailer"
export async function sendVerificationRequest(params) {
const { identifier, url, provider, theme } = params
const { host } = new URL(url)
// 참고: `nodemailer`를 사용할 필요는 없으며, 원하는 것을 사용할 수 있습니다.
const transport = createTransport(provider.server)
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: `Sign in to ${host}`,
text: text({ url, host }),
html: html({ url, host, theme }),
})
const failed = result.rejected.concat(result.pending).filter(Boolean)
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
}
}
function html(params: { url: string; host: string; theme: Theme }) {
const { url, host, theme } = params
const escapedHost = host.replace(/\./g, "​.")
const brandColor = theme.brandColor || "#346df1"
const color = {
background: "#f9f9f9",
text: "#444",
mainBackground: "#fff",
buttonBackground: brandColor,
buttonBorder: brandColor,
buttonText: theme.buttonText || "#fff",
}
return `
<body style="background: ${color.background};">
<table width="100%" border="0" cellspacing="20" cellpadding="0"
style="background: ${color.mainBackground}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center"
style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
Sign in to <strong>${escapedHost}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 5px;" bgcolor="${color.buttonBackground}"><a href="${url}"
target="_blank"
style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${color.buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${color.buttonBorder}; display: inline-block; font-weight: bold;">Sign
in</a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${color.text};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// 이메일 텍스트 본문 (HTML을 렌더링하지 않는 이메일 클라이언트를 위한 대체 텍스트)
function text({ url, host }: { url: string; host: string }) {
return `Sign in to ${host}\n${url}\n\n`
}
React를 사용하여 다양한 이메일 클라이언트와 호환되는 멋진 이메일을 생성하려면 mjml 또는 react-email을 확인해 보세요.
검증 토큰
기본적으로 우리는 무작위 검증 토큰을 생성합니다. 만약 이를 재정의하고 싶다면, 프로바이더 옵션에 generateVerificationToken
메서드를 정의할 수 있습니다:
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
async generateVerificationToken() {
return crypto.randomUUID()
},
}),
],
})
이메일 주소 정규화
기본적으로 Auth.js는 이메일 주소를 정규화합니다. 이메일 주소를 대소문자를 구분하지 않게 처리하며(이는 기술적으로 RFC 2821 스펙을 준수하지 않지만, 실제로는 데이터베이스에서 이메일로 사용자를 조회할 때 더 많은 문제를 일으킬 수 있습니다.), 또한 쉼표로 구분된 목록으로 전달된 보조 이메일 주소를 제거합니다. Nodemailer
프로바이더의 normalizeIdentifier
메서드를 통해 여러분만의 정규화를 적용할 수 있습니다. 다음 예제는 기본 동작을 보여줍니다:
import NextAuth from "next-auth"
import Nodemailer from "next-auth/providers/nodemailer"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Nodemailer({
normalizeIdentifier(identifier: string): string {
// 사용자 입력에서 `@`로 구분된 처음 두 요소만 가져옵니다.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// "@" 앞 부분은 ","를 포함할 수 있지만,
// 도메인 부분에서는 이를 제거합니다.
domain = domain.split(",")[0]
return `${local}@${domain}`
// 오류를 던질 수도 있으며, 이 경우 사용자는
// URL에 error=EmailSignin이 포함된 로그인 페이지로 리디렉션됩니다.
// if (identifier.split("@").length > 2) {
// throw new Error("Only one email allowed")
// }
},
}),
],
})
여러 개의 이메일 주소가 전달되더라도 항상 단일 이메일 주소를 반환하도록 해야 합니다.