devlog study backend nextauth oauth2 server jwt

Auth.js ๐Ÿ“

  • Auth.js - Docs
    • LLM ํ™œ์šฉ ๋‚ด์šฉ ๋ณด์ถฉ

์˜๋ฌธ๊ฐ–๊ธฐ

์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์—ฐ์žฅํ•˜๋ ค๋ฉด access_token์„ ๋งค๋ฒˆ ์ƒˆ๋กœ ๋ฐœ๊ธ‰?

๋ฌธ์ œ :

  • ๋กœ๊ทธ์ธ์ƒํƒœ๋กœ ์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•˜๋ฉด jwt ํ† ํฐ ๊ด€๋ จ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ
// middleware.ts
if (!token) {
  // ์—ฌ๊ธฐ๋กœ ๋น ์ง โ†’ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๊ฐ•์ œ ์ด๋™
  return NextResponse.redirect(
    new URL(`/sign?redirectTo=${pathname}`, req.url)
  );
}
  • ๋งค ์š”์ฒญ๋งˆ๋‹ค access_token์„ ๋ฐœ๊ธ‰ํ•˜๋ฉด:
    • ๋ถˆํ•„์š”ํ•œ ์„œ๋ฒ„ ์ž‘์—…์„ ํ•˜๊ฒŒ๋จ

ํ•ด๊ฒฐ :

middleware์—์„œ ์„ธ์…˜๋งŒ๋ฃŒ์‹œ๊ฐ„์„ ์ฒดํฌ & REFRESH_THRESHHOLD์— ๋งž์ถฐ ์ฟ ํ‚ค๋ฅผ ์ƒˆ๋กœ ๊ตฌ์›€

๐Ÿ”‘ 1. auth.ts - NextAuth ์„ค์ •

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Google({
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",  // ๐Ÿ‘ˆ ์ค‘์š”!
          response_type: "code",
        },
      },
    }),
  ],
});
  • access_type: "offline" ์—ญํ• 
    • Google OAuth์—์„œ Refresh Token์„ ๋ฐ›๊ธฐ ์œ„ํ•œ ์„ค์ •
    • ์ด๊ฒŒ ์—†์œผ๋ฉด access_token๋งŒ ๋ฐ›์•„์™€์„œ ์„ธ์…˜ ๋งŒ๋ฃŒ์‹œ ์žฌ๋กœ๊ทธ์ธ ํ•„์š”
    • offline์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ํ† ํฐ ๊ฐฑ์‹  ๊ฐ€๋Šฅ

๐Ÿ›ก๏ธ 2. middleware.ts - ํ•ต์‹ฌ ๋กœ์ง

  • ๊ธฐ์กด token ์ •๋ณด๋ฅผ ์žฌ์ธ์ฝ”๋”ฉํ•ด์„œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์—ฐ์žฅ
    • encode()ํ•จ์ˆ˜๊ฐ€ ์ƒˆ๋กœ์šด jwt๋ฅผ ์ƒ์„ฑ
    • ์ด๊ฑธ ์ฟ ํ‚ค๋กœ ๋‹ค์‹œ ๊ตฌ์›Œ์„œ ๋ธŒ๋ผ์šฐ์ €์— ์ „๋‹ฌ
  1. ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ โ†’ ๋กœ๊ทธ์ธ ์ฒดํฌ โ†’ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ฒดํฌ
const token = await getToken({ req, secret: SECRET });
 
if (!token && NEED_COOKIES.includes(pathname)) 
  return NextResponse.next();
 
if (!token)
  return NextResponse.redirect(
    new URL(`/sign?redirectTo=${pathname}`, req.url)
  );
 
const REFRESH_THRESHOLD = 10 * 60 * 1000; // 10๋ถ„
const exp = token.exp ? token.exp * 1000 : 0;
 
// ๋งŒ๋ฃŒ ์ž„๋ฐ• : 10๋ถ„ ์ด๋‚ด๋กœ ์ ‘๊ทผํ–ˆ์„ ๊ฒฝ์šฐ์—๋งŒ ๊ฐฑ์‹  โ†’ ์ฟ ํ‚ค ์ƒˆ๋กœ ๊ตฌ์›€
if (exp - Date.now() < MAX_AGE * 1000 - REFRESH_THRESHOLD) {
  const newToken = await encode({ ... });
  res.cookies.set({ ... });
}
  1. ์ฟ ํ‚ค ์ƒˆ๋กœ ๊ตฝ๊ธฐ
const res = NextResponse.next();
const newToken = await encode({
  token,              // ๊ธฐ์กด ํ† ํฐ ์ •๋ณด ์œ ์ง€
  secret: SECRET,
  salt: SALT,
  maxAge: MAX_AGE,    // ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์žฌ์„ค์ •
});
 
// ์ฟ ํ‚ค๊ตฝ๊ธฐ
res.cookies.set({
  name: SALT,      
  value: newToken, // new encoded token
  maxAge: MAX_AGE, // 'undefined'๋ฉด ๋ธŒ๋ผ์šฐ์ € ๋‹ซ์„๋•Œ๊นŒ์ง€ ์œ ์ง€ - e.g. ์ฃผ์ฐจ๋น„ ์ •์‚ฐ์•ฑ
  httpOnly: true,  // XSS๋ฐฉ์–ด - JavaScript๋กœ ์ ‘๊ทผ ๋ถˆ๊ฐ€
  secure: process.env.NODE_ENV === "production", // HTTPS๋งŒ
  sameSite: "lax", // CSRF ๋ฐฉ์–ด
  path: "/",
});

๐Ÿ“Š ์‹ค์ œ ๋™์ž‘ ํƒ€์ž„๋ผ์ธ

1์›” 1์ผ 00:00 - ๋กœ๊ทธ์ธ
โ”œโ”€ ์ฟ ํ‚ค ์ƒ์„ฑ: ๋งŒ๋ฃŒ 1์›” 31์ผ 00:00
โ”‚
1์›” 5์ผ 10:00 - ํŽ˜์ด์ง€ ์ ‘์†
โ”œโ”€ Middleware ์‹คํ–‰
โ”œโ”€ ๋‚จ์€ ์‹œ๊ฐ„: 25์ผ 14์‹œ๊ฐ„
โ”œโ”€ ์กฐ๊ฑด ์ฒดํฌ: 25์ผ < (30์ผ - 10๋ถ„)? YES
โ”œโ”€ โœ… ์ฟ ํ‚ค ์ƒˆ๋กœ ๊ตฌ์›€
โ””โ”€ ์ƒˆ ๋งŒ๋ฃŒ: 2์›” 4์ผ 10:00
โ”‚
1์›” 10์ผ 15:00 - ๋˜ ์ ‘์†
โ”œโ”€ Middleware ์‹คํ–‰
โ”œโ”€ ๋‚จ์€ ์‹œ๊ฐ„: 24์ผ 19์‹œ๊ฐ„
โ”œโ”€ โœ… ์ฟ ํ‚ค ์ƒˆ๋กœ ๊ตฌ์›€
โ””โ”€ ์ƒˆ ๋งŒ๋ฃŒ: 2์›” 9์ผ 15:00

๋‚ด์–ด๋ณด๊ธฐ

OAuth2

  • ๋‹ค๋ฅธ ์„œ๋น„์Šค ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ์„ ํ—ˆ์šฉํ•˜๋Š” ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ

Client : Next.js app / resource Owner : User / Authorization Server : Google server / Resource Server : Google API

๊ธฐ๋ณธ ํ๋ฆ„

RFC ๋‹จ๊ณ„์‹ค์ œ ๋‹จ๊ณ„
(A) Authorization Requestโ‘  ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ โ†’ Provider๋กœ ์ด๋™(๊ถŒํ•œ ์š”์ฒญ ์‹œ์ž‘)
(B) Authorization Grantโ‘ก ์‚ฌ์šฉ์ž๊ฐ€ ๋™์˜ โ†’ ๊ถŒํ•œ ๋ถ€์—ฌ ์‚ฌ์‹ค ํ™•์ •
(C) Authorization Grant โ†’ Auth Serverโ‘ข ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ทธ โ€œ๊ถŒํ•œโ€(=์ฝ”๋“œ)์„ ์„œ๋ฒ„๋กœ ์ œ์ถœ(์ฝ”๋“œ ๊ตํ™˜)
(D) Access Tokenโ‘ฃ ์„œ๋ฒ„๊ฐ€ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์•ก์„ธ์Šค ํ† ํฐ ๋ฐœ๊ธ‰
(E) Access Token โ†’ Resource Serverโ‘ค ์•ก์„ธ์Šค ํ† ํฐ์œผ๋กœ API ์š”์ฒญ
(F) Protected Resourceโ‘ฅ ๋ณดํ˜ธ ์ž์›(ํ”„๋กœํ•„ ๋“ฑ) ์‘๋‹ต : ์„ธ์…˜/JWT ์ƒ์„ฑ โ†’ ์ฟ ํ‚ค(HttpOnly)์— ์ €์žฅ
  • ๋‚ด ์•ฑ์ด ์ง์ ‘ ์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์ง€ ์•Š๊ณ , ํ† ํฐ์„ ํ†ตํ•ด ๊ฐ„์ ‘์ ์œผ๋กœ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ํ™•์ธ

Access Token / Refresh Token

  • Access Token
    • ์œ ํšจ๊ธฐ๊ฐ„์ด ์งง์Œ (์˜ˆ: 1์‹œ๊ฐ„)
    • API ์š”์ฒญํ• ๋•Œ ๋‚˜๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ผ๊ณ  ์ฆ๋ช…ํ•˜๋Š” ์šฉ๋„
  • Refresh Token
    • Access Token ๋งŒ๋ฃŒ๋์„๋•Œ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›๋Š”๋ฐ ์‚ฌ์šฉ
    • ์„œ๋ฒ„์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•ด์•ผ ํ•จ

OAuth2 Provider์—์„œ Access Token์œผ๋กœ ๋ฐ›์•„์˜จ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹

Auth.js ๊ธฐ์ค€

์„ธ์…˜ DB ๋ฐฉ์‹

  • ์„œ๋ฒ„๊ฐ€ DB์— โ€˜์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ƒํƒœโ€™๋ฅผ ์ €์žฅํ•ด๋‘ 
  • ๋ธŒ๋ผ์šฐ์ €์—์„œ ์„ธ์…˜ ID๋ฅผ ์ฟ ํ‚ค๋กœ ๋‚ด๋ ค์คŒ
  • ๋‹ค์Œ ์š”์ฒญ์—์„œ ์ฟ ๋ฆฌ๋ฅผ ๋ณด๊ณ  DB์—์„œ ํ•ด๋‹น ์œ ์ € ์ •๋ณด๋ฅผ ์ฐพ์•„์˜ด

JWT ๋ฐฉ์‹(Auth.js ๊ธฐ๋ณธ)

  • ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ JWT(Json Web Token)์„ ๋ฐœ๊ธ‰
  • JWT๋Š” โ€˜์ด ์œ ์ €๋Š” ๋ˆ„๊ตฌ๋‹คโ€™ ๋ผ๋Š” ์ •๋ณด๋ฅผ ์•”ํ˜ธํ™”๋œ ์„œ๋ช…์œผ๋กœ ๋ณด์ฆ
  • ๋ธŒ๋ผ์šฐ์ € ์ฟ ํ‚ค์— JWT๋ฅผ ๋„ฃ์–ด์คŒ
  • ๋‹ค์Œ ์š”์ฒญ์—์„œ ์„œ๋ฒ„๋Š” JWT๋งŒ ๊ฒ€์ฆ โ†’ DB ์กฐํšŒ ๋ถˆํ•„์š”
  • Access Token = Provider(๊ตฌ๊ธ€/๊นƒํ—ˆ๋ธŒ)์™€ ํ†ต์‹ ์šฉ
  • JWT/์„ธ์…˜ = ์šฐ๋ฆฌ ์•ฑ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋„๊ตฌ

๋ธŒ๋ผ์šฐ์ €์— ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•

  • ์ฟ ํ‚ค(HttpOnly) : ๋ณด์•ˆ์ƒ ์•ˆ์ „, ์ž๋™์œผ๋กœ ์š”์ฒญ์— ๋ถ™์Œ(Auth.js ๊ธฐ๋ณธ)
  • Local storage : ์ง์ ‘ JS๋กœ ์ ‘๊ทผํ•ด์•ผํ•˜๊ณ  ํ•ดํ‚น(XSS)์— ์ทจ์•ฝ โ†’ ์š”์ฆ˜์€ ์•ˆ์”€

NextAuth.js

  • ์„œ๋ฒ„์—์„œ ์„ธ์…˜์„ ์ฟ ํ‚ค ๊ธฐ๋ฐ˜์œผ๋กœ ๊ด€๋ฆฌ
  • ํด๋ผ์ด์–ธํŠธ์—์„œ ์ด ์„ธ์…˜์„ ์ฝ์–ด ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ๋ฅผ ์•Œ์ˆ˜ ์žˆ๊ฒŒ ํ•จ

validator


์ฐธ๊ณ