Javascript/NuxtJS

[Nuxt3] NextAuth, keycloak 연동

kyoulho 2025. 3. 16. 20:26
728x90

NextAuth.js는 OAuth 2.0 및 OpenID Connect(OIDC) 제공자와의 복잡한 인증 과정을 추상화하여 간편하게 설정할 수 있도록 지원하는 라이브러리입니다. 이 글에서는 Nuxt 3 환경에서 Keycloak과 NextAuth.js를 연동하면서, 실제로 구현한 내용을 바탕으로 세부 동작을 설명합니다.
https://auth.sidebase.io/

 

NuxtAuth | Authentication for Nuxt 3

User authentication and sessions via authjs

auth.sidebase.io

NextAuth.js 인증 단계

NextAuth의 역할 자동 여부
로그인 버튼 클릭 (클라이언트에서 signIn() 호출) 🔹커스텀 화면 사용시 개발자 구현
NextAuth 랜딩 화면 사용시 자동 처리
Keycloak 로그인 페이지로 리디렉션 자동 처리
로그인 성공 후 인증 코드를 callback으로 반환 자동 처리
callback에서 액세스 토큰, 리프레시 토큰 발급 자동 처리
발급된 토큰을 JWT 형태로 암호화하여 쿠키에 저장 자동 처리
SSR에서 세션 검증 (getServerSession) 자동 처리
API 호출 시 JWT를 Authorization 헤더에 추가 🔹개발자 구현
액세스 토큰 만료 시 리프레시 토큰으로 갱신 🔹개발자 구현
토큰 만료시 세션 제거 🔹개발자 구현

 

nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@sidebase/nuxt-auth'],
  runtimeConfig: {
    authSecret: '',
    keycloakClientSecret: '',
    keycloakClientId: '',
    keycloakIssuer: '',
  },
  auth: {
    provider: { type: 'authjs' },
    sessionRefresh: {
      enablePeriodically: 1000 * 60 * 5,
      enableOnWindowFocus: true,
    },
    globalAppMiddleware: true,
  },
});
  • defaultProvider를 설정하지 않으면 nextAuth에서 제공하는 랜딩화면을 사용할 수 있다.
  • defaultProvider를 설정하면 로그아웃 시에 다시 OAuth Provider에 로그인 상태를 확인하게 되므로 로그아웃이 되지 않는다. 필요하면 OAuth Provider의 세션을 제거해야한다.

/server/api/auth/[...].ts

import { NuxtAuthHandler } from '#auth';
import type { JWT } from 'next-auth/jwt';
import KeycloakProvider from 'next-auth/providers/keycloak';

export default NuxtAuthHandler({
  secret: useRuntimeConfig().authSecret,
  providers: [
    KeycloakProvider.default({
      clientId: useRuntimeConfig().keycloakClientId,
      clientSecret: useRuntimeConfig().keycloakClientSecret,
      issuer: useRuntimeConfig().keycloakIssuer,
    }),
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      // 로그인 시에만 accout 값이 존재함.
      // Access Token 및 Refresh Token 저장
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.accessTokenExpires = account.expires_at! * 1000;
      }

      if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
        return token;
      }

      // Access Token이 만료되었으면 Refresh Token을 사용해 갱신
      return await refreshAccessToken(token);
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.accessTokenExpires = token.accessTokenExpires;
      return session;
    },
  },
});

async function refreshAccessToken(token: JWT): Promise<JWT> {
  const config = useRuntimeConfig();

  try {
    const response = await fetch(`${config.keycloakIssuer}/protocol/openid-connect/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        client_id: config.keycloakClientId,
        client_secret: config.keycloakClientSecret,
        grant_type: 'refresh_token',
        refresh_token: token.refreshToken!,
      }),
    });

    const data = await response.json();

    if (!response.ok) {
      const error = data.error_description || data.error || 'Failed to refresh access token';
      throw new Error(error);
    }

    return {
      ...token,
      accessToken: data.access_token,
      accessTokenExpires: Date.now() + data.expires_in * 1000,
      refreshToken: data.refresh_token ?? token.refreshToken,
    };
  } catch (error) {
    console.error('토큰 갱신 실패:', error);
    return { ...token };
  }
}

token

  • JWT에는 사용자의 기본 정보(예: 이름, 이메일, 사용자 ID 등)가 포함됩니다.
  • 이 JWT는 next-auth.session-token이라는 이름의 쿠키에 암호화되어 저장되어, 클라이언트가 인증 상태를 유지할 수 있도록 합니다.

account

  • 사용자가 OAuth 제공자(Keycloak 등)를 통해 로그인할 때, 해당 제공자로부터 받은 정보를 담고 있습니다. 예를 들어, 로그인 직후 Keycloak에서 전달받은 access_token, refresh_token, 만료 시간 등의 데이터가 포함됩니다.
  • 초기 로그인 시에만 제공되고, 이후에는 개발자가 jwt 콜백 내에서 account 정보를 token에 병합(merge)하여 저장하도록 구현해야 합니다. 이렇게 함으로써 account의 정보가 휘발되지 않고, 향후 세션 관리에 사용될 수 있습니다.

session

  • session 객체는 클라이언트 측에서 getServerSession() 또는 getSession() 함수를 호출할 때 반환되는 객체입니다.
  • 이 session은 next-auth.session-token 쿠키에 저장된 JWT를 파싱하여 token 기반으로 만들어지며, 클라이언트가 인증 상태와 필요한 사용자 정보를 확인할 수 있도록 제공합니다.
  • 토큰의 만료 시간과 세션의 만료 시간은 상이합니다. 때문에 토큰은 만료되었지만 세션은 살아있을 수 있으며 세션에 토큰 만료시간을 담아 미들웨어 등에서 확인 signOut()을 호출하여 세션과 토큰의 수명을 같게 할 수 있습니다.

/types/next-auth.d.ts

import 'next-auth';
import 'next-auth/jwt';

declare module 'next-auth' {
  // getSession(), getServerSession()으로 반환되는 객체
  interface Session {
    accessToken?: string;
    accessTokenExpires?: number;
  }
}

declare module 'next-auth/jwt' {
  // next-auth.session-token 으로 쿠키에 암호화 되어 저장되는 객체
  interface JWT {
    accessToken?: string;
    refreshToken?: string;
    accessTokenExpires?: number;
  }
}
728x90

'Javascript > NuxtJS' 카테고리의 다른 글

[Nuxt] 빌드 후 동적 프록시 설정하기  (0) 2025.03.26
[Nuxt3] 환경 변수 관리  (0) 2025.03.15
[Nuxt3] 스토리지: vueuse  (0) 2025.03.15
[Nuxt3] 헬스 체크  (1) 2025.03.15
[Nuxt3] 데이터 요청  (0) 2025.03.15