코딩/프로젝트

[Supabse] OAuth 로그인 후 profile 데이터 추가

김기지 2025. 1. 22. 18:11

Supabase에서 사용자 인증을구현하는 방식에는 2가지가 있다.

Implicit flow

브라우저 단에서 로그인을 진행 → 서버가 필요 없음(supabase가 역할을 대신해주기 때문)

클라이언트 안에 액세스 토큰과 리프레시 토큰을 보관하기 때문에 한계점이 존재!!(보안상 이슈 등)

 

PKCE flow

implicit flow의 한계점을 극복하기 위해 도입된 인증 방법

Authorization Code Flow를 사용할 때 코드 교환 과정의 보안을 강화하기 위해 OAuth 2.1에서 필수적으로 사용하도록 지정되어 있음

 


프로젝트에서 Supabase Auth의 구글과 카카오 provider를 사용해 소셜 로그인을 구현했다.

// supabase/client
export const createSupabaseBrowserClient = () =>
    createBrowserClient<Database>(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    )

...

// actions/login
'use client'

export async function signInWithGoogle(){
    const supabase = createSupabaseBrowserClient();
    const { error } = await supabase.auth.signInWithOAuth({
        provider: 'google',
        options:{
            redirectTo:process.env.NEXT_PUBLIC_AUTH_REDIRECT_TO,
            queryParams:{
                access_type:'offline',
                prompt: 'consent'
            }
        }
    });
    if(error) console.log('google login error:',error.message); 
    
}

export async function signInWithKakao(){ 
    const supabase = createSupabaseBrowserClient();
    const {error} = await supabase.auth.signInWithOAuth({
        provider:'kakao',
        options:{
            redirectTo:process.env.NEXT_PUBLIC_AUTH_REDIRECT_TO,
            queryParams:{
                access_type:'offline',
                prompt: 'consent'
            }
        }
    });
    if(error) console.log('kakao login error:',error.message);
    
    
}

export async function signOut(){
    const supabase = createSupabaseBrowserClient();
    const {error} = await supabase.auth.signOut();
    if(error){
        console.log(error);
    }
}

로그인 버튼에 각 액션을 연결하고 redirect 할 콜백 url을 넘겨준다.

 

로그인이 완료되면 auth의 콜백 경로로 리다이렉트 하도록 설정했다.

NEXT_PUBLIC_AUTH_REDIRECT_TO=https://localhost:3000/auth/callback?next=/stadium

 

  • callback?next=/ ⇨ 콜백 url을 통해 어떠한 로직을 처리한 다음 다시 뒤에 오는 경로로 이동할 수 있도록 함

auth 폴더 안에 callback url이 떨어졌을 때 처리할 route 핸들러를 작성했다.

이때 로그인된 유저의 profile 정보가 테이블에 없는 경우 로그인된 유저 정보를 가져와 테이블에 추가한다.

import { createServerSideClient } from '@/app/utils/server';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get('code');
  const next = searchParams.get('next') ?? '/';

  if (code) {
      const supabase =await createServerSideClient();

      const {  error } =await supabase.auth.exchangeCodeForSession(code);

      if (!error) {
        // user 정보 가져오기
        const {data:{user}} = await supabase.auth.getUser();
       
        if(!user){
          return NextResponse.redirect(`${origin}/auth`);
        }

        const { id, email, user_metadata } = user;
        const { name, avatar_url } = user_metadata;

        // 존재하는 user인지 확인
        const {data} = await supabase
        .from('profiles')
        .select('id')
        .eq('id', id)
        .single();


        if(!data){
          const { error} = await supabase
          .from('profiles')
          .insert({
            id,
            email,
            name,
            avatar_url
          });
   
          if(error){
            console.log('error>>>>', error)
          }
        }

        const forwardedHost = request.headers.get('x-forwarded-host');
        const isLocalEnv = process.env.NODE_ENV === 'development';
        if (isLocalEnv) { 
          return NextResponse.redirect(`${origin}${next}`);
        } else if (forwardedHost) {
          return NextResponse.redirect(`https://${forwardedHost}${next}`);
        } else {
          return NextResponse.redirect(`${origin}${next}`);
        }
      };
    }


  return NextResponse.redirect(`${origin}/auth`);
}
  • exchangeCodeForSession -> 토큰을 교환해주는 함수

에러가 발생한 경우 원래 경로로 리다이렉트 되고 로그인이 완료되면 설정한 url로 리다이렉트 된다.

 

 

728x90