Skip to main content

Supabase Auth

Goal

1.로그인

  • email login
  • google login ✅
  • github login ✅
  • kakao login (사업자 등록필요.?!)

2.로그인 세션, 유지

  • getUser vs getSession
  • 리프레시 토큰 어떻게 로그인세션이 다시 갱신 되는가 ?

3.로그아웃

  • 세션정보 어떻게 날라가는가?
  • 세션 아웃 - 얼마 후 자동 로그아웃 되는가 ?
  • 세션 아웃 - 설정이 가능한가?

*OAuth 2.0, 2.1, PKCE 플로우 등 관련 이론은 다음 장에서 다룬다.

install

// (optional install) 로그인 관련 UI 제공  
yarn add @supabase/auth-ui-react // 로그인 UI제공
yarn add @supabase/auth-ui-shared // 테마 제공

📌 로그인 공통 작업

Login flow Overview

최대한 많은 부분을 서버사이드 처리를 원칙으로 한다.

⚠️ 주의) getSession vs getUser

  • 페이지를 보호할 때는 주의하세요. 서버는 누구든지 스푸핑할 수 있는 쿠키로부터 사용자 세션을 가져옵니다.
  • 페이지와 사용자 데이터를 보호하려면 항상 supabase.auth.getUser()를 사용하세요.
  • 미들웨어와 같은 서버 코드 내부의 supabase.auth.getSession()을 절대 신뢰하지 마십시오. 인증 토큰 재검증이 보장되지는 않습니다.
  • getUser()는 인증 토큰을 재검증하기 위해 매번 Supabase 인증 서버에 요청을 보내기 때문에 신뢰하는 것이 안전합니다.

Common Logic

  • 아래 로직은 공통 로직으로 필수 입니다.

1.env

// .env
NEXT_PUBLIC_SUPABASE_URL=https://YOURS.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOURS

//1.배포되는 환경의 도메인 주소
NEXT_PUBLIC_ORIGIN=http://YOUR_DOMAIN

//2.PKCE Callback을 처리할 도메인 주소
NEXT_PUBLIC_AUTH_REDIRECT_TO_PKCE=http://YOUR_DOMAIN/auth/callback?next=/

2.PKCE Callback

// app/auth/callback/route.ts 
import { NextResponse } from "next/server";
import { createServerSideClient } from "@/lib/supabase";

export async function GET(request: Request) {
const overrideOrigin = process.env.NEXT_PUBLIC_ORIGIN;
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) return NextResponse.redirect(`${overrideOrigin}`);

return NextResponse.redirect(`${overrideOrigin}${next}`);
}
return NextResponse.redirect(`${overrideOrigin}`);
}

3.LoginUI

using Auth UI ( @supabase/auth-ui-react )

// components/auth-modal.tsx

'use client';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Session, User } from '@supabase/supabase-js';

import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { DotLoader } from 'react-spinners';
import { createSupabaseBrowserClient } from '@/lib/supabase/browser-client';
import useHydrate from '@/hooks/useHydrate';
import { useEffect, useState } from 'react';

interface AuthHeaderProps {}

export function AuthModal({}: AuthHeaderProps) {
const isHydrate = useHydrate();
const supabase = createSupabaseBrowserClient();
const [userSession, setUserSession] = useState<Session | null>(null);
const user = userSession?.user;
const isLogin = user?.email;

// without auth-ui
const handleGoogleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: process.env.NEXT_PUBLIC_AUTH_REDIRECT_TO_PKCE,
},
});
};

const handleLogout = async () => {
await supabase.auth.signOut();
window.location.reload();
};

useEffect(() => {
const getUserSession = async () => {
const { data: userSession } = await supabase.auth.getSession();

if (userSession) setUserSession(userSession?.session);
};
getUserSession();
}, []);

if (!isHydrate) return <DotLoader color={'white'} size={16} />;

return (
<Dialog>
{!isLogin && (
<DialogTrigger asChild>
<Button variant="outline">
<div className="font-bold text-[16px] cursor-pointer">Login</div>
</Button>
</DialogTrigger>
)}
{isLogin && (
<Button onClick={handleLogout}>
<div className="font-bold text-[16px] cursor-pointer">Logout</div>
<div>({user?.email})</div>
</Button>
)}

<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Welcome</DialogTitle>
<DialogDescription>Login completed in 3 seconds!</DialogDescription>
<Auth
redirectTo={process.env.NEXT_PUBLIC_AUTH_REDIRECT_TO_PKCE}
supabaseClient={supabase}
appearance={{
theme: ThemeSupa,
}}
onlyThirdPartyProviders
providers={['google']}
/>
</DialogHeader>
</DialogContent>
</Dialog>
);
}

cf

4.White Listing Redirect URLs

브라우저에서 OAuth 로그인 시도 후 성공 후, 새로운 경로로 리다이렉트한다.

  • 예) 아래 주소는 로그인 시도 후 성공했을때 이동하는 경로이다.
  • http://localhost:3000/auth/callback?code=45c150a1-85e1-4e95-bcc0-1a1c9646b2da
    • code값이 포함되어 있는데 이는 PKCE flow에 사용된다.
    • 하지만 위 http://localhost:3000/auth/callback 로 리다이렉트를 위해 화이트리스팅 설정이 필요.

Alt text
Site URL

  • 아래 Redirect URLs 에 없는 주소로 redirectTo 설정을 하게 되면 기본값(Site URL)로 리다이렉트 된다.

Redirect URLs

  • http://localhost:3000/api/auth/callback 를 추가해주자.
  • 코드의 redirectTo에 위 주소를 적게 되면 정상작동하게 된다.

📌 Google Login

해야할 작업

  • 1.구글 클라우드 셋팅 + supabase Provider 셋팅
  • 2.AuthUI
  • 3.callback 처리

참고 문서

1.구글 클라우드 셋팅 + supabase Provider 셋팅

Google Cloud 설정   
- *새로운 프로젝트 만들기

1.API 및 서비스 > OAuth 동의 화면 탭
- *승인된 도메인 입력 : Project_URL.supabase.co

2.API 및 서비스 > 사용자 인증 정보 탭
Google Cloud에서 supabase Providers에 설정하기 위함

2.1 사용자 인증 정보 만들기 > OAuth 2.0 클라이언트 ID > 생성
- (Google Cloud 정보 --> supabase 설정)
- 1.클라이언트 ID > Client ID (for OAuth)
- 2.클라이언트 보안 비밀번호 > Client Secret (for OAuth)

3.supabase 돌아와서 > Authentication > Providers 탭
- (supabase 정보 --> Google Cloud 설정) 설정해야 합니다.
- 1.Callback URL (for OAuth) 복사 > 승인된 리디렉션 URI 넣기

이하 공통로직를 따른다.

  • 2.1 로그인 코드 작성 - 공통로직의 AuthUI 코드
  • 2.2 Redirect URLs 설정 - 공통로직 = White Listing Redirect URLs
  • 3.1 PKCE Callback - 공통로직 = PKCE Callback

📌 kakao Login

kakao developers 접속 : https://developers.kakao.com/

kakao developers 설정   
0.새로운 애플리케이션 추가하기
- *아이콘을 필수로 올리기

1.앱 설정>플랫폼 탭
- Web 사이트 도메인 추가 eg) http://YOURS.supabase.co

2.Supabase의 Client ID, Client Secret 설정해야 합니다.
- (kakao developers 정보 --> supabase 설정)
- 앱 설정 > 앱 키 탭
- 1.REST API 키 > Client ID (for OAuth)
- 제품 설정>카카오 로그인>보안 탭
- 2.Client Secret 발급 > Client Secret (for OAuth)

3.제품 설정 > 카카오 로그인 탭
- (supabase 정보 --> kakao developers 설정) 설정해야 합니다.
- 1.Callback URL (for OAuth) > Redirect URI 추가 eg) https://YOURS.supabase.co/auth/v1/callback
- 2.카카오 로그인 > 활성화 설정 > ON

4.임시로 비즈앱으로 전환 및 개인정보 동의를 받아야 합니다.

4.1 앱 설정 > 비즈니스
- 개인 개발자 비즈 앱 전환 (완료하기)

4.2 앱 설정>앱 권한 신청 탭
- 신청 자격 확인 (완료하기)

4.3 제품 설정>카카오 로그인>동의항목 탭

동의항목에서 다음 필수 체크
- profile_nickname
- profile_image
- account_email

Alt text

이하 공통로직를 따른다.

  • 2.1 로그인 코드 작성 - 공통로직의 AuthUI 코드
  • 2.2 Redirect URLs 설정 - 공통로직 = White Listing Redirect URLs
  • 3.1 PKCE Callback - 공통로직 = PKCE Callback

📌 Github Login

OAuth App -> https://github.com/settings/developers

Github OAuth App 설정   
- *New OAuth App

1.Register a new OAuth app 탭
- *Application name : 원하는 것으로
- *Homepage URL : http://localhost:3000/
- (supabase 정보 --> Github OAuth App 설정)
- *Authorization callback URL : Project_URL.supabase.co

2.General 탭
- (Github OAuth App 정보 --> supabase 설정)
- 1.Client ID > Client ID
- 2.Client secrets > Client Secret

이하 공통로직를 따른다.

  • 2.1 로그인 코드 작성 - 공통로직의 AuthUI 코드
  • 2.2 Redirect URLs 설정 - 공통로직 = White Listing Redirect URLs
  • 3.1 PKCE Callback - 공통로직 = PKCE Callback

📌 Email login

과정

  • 회원가입 > 이메일 인증 대기 > 인증 완료시 DB 업데이트

코드 작업

  • 1.AuthUI or signInWithPassword
  • 2.callback 처리

1.AuthUI or signInWithPassword

LoginUI 동일

2.callback 처리

참조-공식문서 - https://supabase.com/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr#create-api-endpoint-for-handling-tokenhash

// app/auth.confirm/route.ts
import { type EmailOtpType } from "@supabase/supabase-js";
import { type NextRequest, NextResponse } from "next/server";

import { createServerSideClient } from "@/lib/supabase";

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType | null;
const next = searchParams.get("next") ?? "/";

const redirectTo = request.nextUrl.clone();
redirectTo.pathname = next;
redirectTo.searchParams.delete("token_hash");
redirectTo.searchParams.delete("type");

if (token_hash && type) {
const supabase = createServerSideClient();

const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
});
if (!error) {
redirectTo.searchParams.delete("next");
return NextResponse.redirect(redirectTo);
}
}

// return the user to an error page with some instructions
redirectTo.pathname = "/error";
return NextResponse.redirect(redirectTo);
}


TroubleShooting

1.주의 브라우저, 서버 모듈 분리

lib안에서 슈파베이스 서버용,브라우저용 클라이언트 모듈을 따로 분리하자.

  • 하나의 파일 안에 createBrowserClient, createServerClient를 동시에 정의를 못한다.
  • *하나의 파일(모듈)안에 next/headers을 사용하는 순간, 서버전용모듈이 된다.

'use client', 'use server' 지시어란?

  • 참고 : https://react.dev/reference/react/use-server
  • 'use server'는 모듈(파일)단위, 함수단위에서 적용 가능하다.
  • 하지만 나는 파일단위로 구분짓고 싶다. 파일안에 특정 함수에 'use server'을 넣는것은 아직 원치 않는다.

2.주의 UI 깨지는 이슈

문제 : SSR된 Auth 컴포넌트는 스타일이 깨지는 이슈가 있다.

  • 그래서 hydration이 끝나면 보여주도록 하자.
import { useEffect, useState } from "react";

const useHydrate = () => {
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

return isMounted;
};

export default useHydrate;

3.주의 SiteURL, RedirectURLs 설정

Alt text

Site URL

  • Next.js의 주소를 입력해준다.
  • 로컬 개발 환경 : http://localhost:3000
  • 배포된 환경 : https://배포된주소.vercel.com
  • 슈파베이스 인증 서버가 인증 후 redirect를 위 경로로 해준다. (디폴트)

Redirect URLs

  • 위 설정을 해야, 코드단 옵션 중 redirectTo가 작동한다.
  • *만약 위 설정을 해주지 않으면, 코드에서 redirectTo설정 및 배포를 해도 localhost로 계속 리다이렉트 될 것이다.
  • PKCE Flow 를 처리하기 위해 설정해준다.