Supabase:Auth
Managing User Data
Server-Side Rendering
사용자가 Supabase Auth로 인증하면 서버에서 두 가지 정보가 발행됩니다.
- Access Token 은 JWT 형식 입니다.
- Refresh Token 은 랜덤으로 생성된 문자열 입니다.
대부분의 Supabase 프로젝트는 <project-ref>.supabase.co/auth/v1
에서 Listening 중인 인증 서버가 있다.
따라서 액세스 토큰 및 새로 고침 토큰은 <project-ref>.supabase.co
도메인으로 sb-access-token
및 sb-refresh-token
쿠키로 설정됩니다.
WARNING |
이러한 쿠키 이름은 Supabase 전용으로 사용되며, 경고 없이 변경될 수 있습니다. 설명 목적으로만 이용해야 한다. |
웹 브라우저는 SOP (Same-Origin Policy) 에 따라 도메인 전체에서 쿠키에 대한 액세스를 제한합니다. 귀하의 웹 애플리케이션은 이러한 쿠키에 액세스할 수 없으며 이러한 쿠키는 귀하의 애플리케이션 서버로 전송되지 않습니다.
인증 흐름 이해
signIn
메서드 중 하나를 호출하면, 브라우저에서 실행 중인 클라이언트 라이브러리가 Supabase Auth 서버로 요청을 보냅니다.
인증 서버는 전화번호, 이메일 및 비밀번호 조합, Magic Link 또는 소셜 로그인(프로젝트에 설정이 있는 경우)을 사용할지 여부를 결정합니다.
사용자 ID가 성공적으로 확인되면 Supabase Auth 서버는 사용자를 다시 SPA (Single-page app) 으로 리디렉션 합니다.
Tip |
Supabase 대시보드에서 리디렉션 URL을 구성할 수 있습니다. |
Supabase Auth는 암시적 (Implicit) 및 #PKCE Flow의 두 가지 인증 흐름을 지원합니다.
#PKCE Flow 흐름은 일반적으로 서버에 있을 때 선호됩니다. Replay attack 및 URL capture 공격으로부터 보호하는 몇 가지 추가 단계를 소개합니다.
"암시적 흐름 (Implicit flow)"와 달리 사용자가 서버의 access_token
및 refresh_token
에 액세스할 수도 있습니다.
Implicit Flow
암시적 흐름을 사용하는 경우 다음 구조로 리디렉션 URL이 반환됩니다.
성공적인 확인 후 첫 번째 액세스 및 새로 고침 토큰은 리디렉션 위치의 URL Fragment(#
기호 뒤의 모든 항목)에 포함됩니다. 이것은 의도적이며 수정할 수 없습니다.
클라이언트 라이브러리는 이러한 유형의 URL을 수신하고 여기에서 "액세스 토큰" 과 "새로 고침 토큰" 및 일부 추가 정보를 추출하고 마지막으로 라이브러리 및 앱에서 추가로 사용할 수 있도록 로컬 스토리지에 유지하도록 설계되었습니다.
- 상세 정보
- 웹 브라우저는 요청을 보내는 서버에 URL 조각을 보내지 않습니다.
- 귀하가 직접 제어하는 서버(예: GitHub Pages 또는 기타 부분 유료화 호스팅 공급자)에서 "SPA (단일 페이지 앱)"을 호스팅하지 않을 수 있으므로 기본적으로 호스팅 서비스가 사용자의 인증 자격 증명에 액세스하지 못하도록 합니다.
- 서버가 직접 제어되는 경우에도 GET 요청과 해당 전체 URL이 기록되는 경우가 많습니다.
- 이 접근 방식은 또한 요청 또는 액세스 로그에서 자격 증명 유출을 방지합니다. 서버에서
access_token
및refresh_token
를 얻으려면 #PKCE Flow 을 사용하는 것이 좋습니다.
PKCE Flow
PKCE 흐름을 사용하는 경우 다음 구조로 리디렉션 URL이 반환됩니다.
이 code
매개변수는 일반적으로 "인증 코드 (Auth Code)"로 알려져 있으며 exchangeCodeForSession(code)
를 호출하여 액세스 토큰으로 교환할 수 있습니다.
INFORMATION |
보안을 위해 코드는 5분 동안 유효하며 한 번만 액세스 토큰으로 교환할 수 있습니다. 새 액세스 토큰을 얻으려면 인증 흐름을 처음부터 다시 시작해야 합니다. |
플로우가 Server-Side 에서 실행되므로 localStorage를 사용하지 못할 수 있습니다.
storage
옵션으로 사용자 정의 스토리지를 클라이언트 라이브러리에 구성할 수 있습니다.
const customStorageAdapter: SupportedStorage = {
getItem: (key) => {
if (!supportsLocalStorage()) {
// Configure alternate storage
return null
}
return globalThis.localStorage.getItem(key)
},
setItem: (key, value) => {
if (!supportsLocalStorage()) {
// Configure alternate storage here
return
}
globalThis.localStorage.setItem(key, value)
},
removeItem: (key) => {
if (!supportsLocalStorage()) {
// Configure alternate storage here
return
}
globalThis.localStorage.removeItem(key)
},
}
성공적인 리디렉션 후 클라이언트 라이브러리를 세션과 자동으로 교환하도록 클라이언트 라이브러리를 구성할 수도 있습니다. detectSessionInUrl
옵션을 true
로 설정하면 됩니다.
모두 종합하면 클라이언트 라이브러리 초기화는 다음과 같습니다:
const supabase = createClient(
'https://xyzcompany.supabase.co',
'public-anon-key',
options: {
...
auth: {
...
detectSessionInUrl: true,
flowType: 'pkce',
storage: customStorageAdapter,
}
...
}
)
자세한 내용은 PKCE를 참조.
Row Level Security
- Row Level Security | Supabase Docs
- Supabase와 PostGreSQL의 Row Level Security 살펴보기
- [추천] Managing User Data | Supabase Docs
세분화된 권한 부여 규칙이 필요한 경우 PostgreSQL의 RLS (Row Level Security)를 능가하는 것은 없습니다 .
정책은 PostgreSQL의 규칙 엔진입니다. 매우 강력하고 유연하여 고유한 비즈니스 요구에 맞는 복잡한 SQL 규칙을 작성할 수 있습니다.
RLS는 다음과 같이 활성화 할 수 있습니다:
Policies
정책은 요령을 터득하면 쉽게 이해할 수 있습니다. 각 정책은 테이블에 연결되며 테이블에 액세스할 때마다 정책이 실행됩니다.
모든 쿼리에 WHERE절을 추가하는 것으로 생각할 수 있습니다.
예를 들어 다음과 같은 정책이 있을 때:
create policy "Individuals can view their own todos."
on todos for select
using ( auth.uid() = user_id );
사용자가 todos 테이블에서 선택하려고 할 때마다 다음과 같이 변환됩니다:
이 패턴의 좋은 점은? 이제 API를 통해 이 테이블을 쿼리할 수 있으며 API 쿼리에 데이터 필터를 포함할 필요가 없습니다. 정책이 이를 처리합니다.
// 사용자가 로그아웃했다면 아무 것도 반환하지 않습니다.
const { data } = await supabase.from('profiles').select('id, username, avatar_url, website')
// 사용자가 로그인한 후에만 정상 데이터가 반환됩니다.
// 로그인한 사용자의 데이터 - 이 경우 단일 행
const { error } = await supabase.auth.signIn({ email })
const { data: profile } = await supabase
.from('profiles')
.select('id, username, avatar_url, website')
행 수준 보안 우회
전체 사용자 프로필 목록을 가져와야 하는 경우 API 및 클라이언트 라이브러리와 함께 사용하여 행 수준 보안을 우회할 수 있는 service_key
를 제공합니다.
WARNING |
이것을 공개적으로 노출하지 마십시오. 그러나 모든 프로필을 가져오기 위해 서버 측에서 사용할 수 있습니다. |
Accessing User Metadata
사용자 메타 데이터 접근 방법.
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'example-password',
options: {
data: {
first_name: 'John',
age: 27,
},
},
})
사용자 메타데이터는 auth.users
테이블의 raw_user_meta_data
열에 저장됩니다. 메타데이터를 보려면:
새로운 사용자 이벤트 트리거
사용자가 가입할 때마다 public.profiles
테이블에 행을 추가하려면 트리거를 사용할 수 있습니다.
그러나 트리거가 실패하면 사용자 가입이 차단될 수 있으므로 코드가 제대로 테스트되었는지 확인하십시오.
For example:
-- inserts a row into public.profiles
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
insert into public.profiles (id)
values (new.id);
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
Metadata 수정/삭제
auth.users
테이블의 raw_user_meta_data
컬럼에 jsonb
타입으로 저장된다.
추가 or 수정:
삭제:
INFORMATION |
저렇게 명시적으로 추가된 내용이 아닌 Json 프로퍼티 들은 변경되지 않는다. 그러니 |
OAuth : Identity linking strategies
- Identity Linking | Supabase Docs # Identity linking strategies
- Supabase Auth: Identity Linking, Hooks, and HaveIBeenPwned integration
Helper
Supabase는 정책과 함께 사용할 수 있는 몇 가지 쉬운 기능을 제공합니다.
-
auth.uid()
- 요청을 하는 사용자의 ID를 반환합니다. -
auth.jwt()
- 요청하는 사용자의 JWT를 반환합니다.
Supabase Auth with the Next.js App Router
Middleware
import {createMiddlewareClient} from '@supabase/auth-helpers-nextjs';
import type {NextRequest} from 'next/server';
import {NextResponse} from 'next/server';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
// Create a Supabase client configured to use cookies
const supabase = createMiddlewareClient({req, res});
// Refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
await supabase.auth.getSession();
return res;
}
처음 res
값은:
{
cookies: ResponseCookies {},
url: '',
body: null,
bodyUsed: false,
headers: { x-middleware-next: '1' },
ok: true,
redirected: false,
status: 200,
statusText: '',
type: 'default'
}
supabase.auth.getSession
이 후, res
값이 아래와 같이 cookie
, set-cookie
가 추가된다. 해당 키는 Set-Cookie 헤더가 매칭된다.
{
cookies: ResponseCookies {},
url: '',
body: null,
bodyUsed: false,
headers: {
cookie: 'sb-zmbedptdtgbeubeigtge-auth-token=%5B%22eyJhbGciOiJIUzI1NiIsImtpZCI6ImRmS0NVcHk4NkphdndIRFQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjkyNDA1NTU4LCJpYXQiOjE2OTI0MDE5NTgsImlzcyI6Imh0dHBzOi8vaHR0cHM6Ly96bWJlZHB0ZHRnYmV1YmVpZ3RnZS5zdXBhYmFzZS5jby9hdXRoL3YxIiwic3ViIjoiNzkyMWE4ZjQtMGUzYi00NzlkLWJlNDItY2YwNmNiMDI0ZDBkIiwiZW1haWwiOiJvc29tODk3OUBnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY5MjM2NTk0MX1dLCJzZXNzaW9uX2lkIjoiYzUwMWY0MzktOGZmNS00N2JmLTljZGQtNzBhNzZkZTE3ZjRjIn0.TBvVeBT-qKySQwLDQ3DM4ZVEfvShmuT56r081R7vaPk%22%2C%22LB8TC-eJUhFAOPvaH6OjsA%22%2Cnull%2Cnull%2Cnull%5D; Max-Age=31536000000; Path=/',
set-cookie: 'sb-zmbedptdtgbeubeigtge-auth-token=%5B%22eyJhbGciOiJIUzI1NiIsImtpZCI6ImRmS0NVcHk4NkphdndIRFQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjkyNDA1NTU4LCJpYXQiOjE2OTI0MDE5NTgsImlzcyI6Imh0dHBzOi8vaHR0cHM6Ly96bWJlZHB0ZHRnYmV1YmVpZ3RnZS5zdXBhYmFzZS5jby9hdXRoL3YxIiwic3ViIjoiNzkyMWE4ZjQtMGUzYi00NzlkLWJlNDItY2YwNmNiMDI0ZDBkIiwiZW1haWwiOiJvc29tODk3OUBnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY5MjM2NTk0MX1dLCJzZXNzaW9uX2lkIjoiYzUwMWY0MzktOGZmNS00N2JmLTljZGQtNzBhNzZkZTE3ZjRjIn0.TBvVeBT-qKySQwLDQ3DM4ZVEfvShmuT56r081R7vaPk%22%2C%22LB8TC-eJUhFAOPvaH6OjsA%22%2Cnull%2Cnull%2Cnull%5D; Max-Age=31536000000; Path=/',
x-middleware-next: '1'
},
ok: true,
redirected: false,
status: 200,
statusText: '',
type: 'default'
}
Creating a Supabase Client
Next.js 인증 도우미를 사용하여 Supabase 클라이언트에 액세스하는 5가지 방법이 있습니다.:
- Client Components - createClientComponentClient in Client Components
- Server Components - createServerComponentClient in Server Components
- Server Actions - createServerActionClient in Server Actions
- Route Handlers - createRouteHandlerClient in Route Handlers
- Middleware - createMiddlewareClient in Middleware
Client Components
'use client'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useEffect, useState } from 'react'
import type { Database } from '@/lib/database.types'
type Todo = Database['public']['Tables']['todos']['Row']
export default function Page() {
const [todos, setTodos] = useState<Todo[] | null>(null)
const supabase = createClientComponentClient<Database>()
useEffect(() => {
const getData = async () => {
const { data } = await supabase.from('todos').select()
setTodos(data)
}
getData()
}, [])
return todos ? <pre>{JSON.stringify(todos, null, 2)}</pre> : <p>Loading todos...</p>
}
Server Components
import { cookies } from 'next/headers'
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '@/lib/database.types'
export const dynamic = 'force-dynamic'
export default async function ServerComponent() {
const supabase = createServerComponentClient<Database>({ cookies })
const { data } = await supabase.from('todos').select()
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
Server Actions
WARNING |
2023-12-28 기준 서버 액션에서 createServerActionClient를 사용해도 캐시 갱신이 안되더라... |
기본 auth.users
테이블 스키마
create table
auth.users (
instance_id uuid null,
id uuid not null,
aud character varying(255) null,
role character varying(255) null,
email character varying(255) null,
encrypted_password character varying(255) null,
email_confirmed_at timestamp with time zone null,
invited_at timestamp with time zone null,
confirmation_token character varying(255) null,
confirmation_sent_at timestamp with time zone null,
recovery_token character varying(255) null,
recovery_sent_at timestamp with time zone null,
email_change_token_new character varying(255) null,
email_change character varying(255) null,
email_change_sent_at timestamp with time zone null,
last_sign_in_at timestamp with time zone null,
raw_app_meta_data jsonb null,
raw_user_meta_data jsonb null,
is_super_admin boolean null,
created_at timestamp with time zone null,
updated_at timestamp with time zone null,
phone text null default null::character varying,
phone_confirmed_at timestamp with time zone null,
phone_change text null default ''::character varying,
phone_change_token character varying(255) null default ''::character varying,
phone_change_sent_at timestamp with time zone null,
confirmed_at timestamp with time zone null,
email_change_token_current character varying(255) null default ''::character varying,
email_change_confirm_status smallint null default 0,
banned_until timestamp with time zone null,
reauthentication_token character varying(255) null default ''::character varying,
reauthentication_sent_at timestamp with time zone null,
is_sso_user boolean not null default false,
deleted_at timestamp with time zone null,
constraint users_pkey primary key (id),
constraint users_phone_key unique (phone),
constraint users_email_change_confirm_status_check check (
(
(email_change_confirm_status >= 0)
and (email_change_confirm_status <= 2)
)
)
) tablespace pg_default;
create unique index confirmation_token_idx on auth.users using btree (confirmation_token)
where
((confirmation_token)::text !~ '^[0-9 ]*$'::text) tablespace pg_default;
create unique index email_change_token_current_idx on auth.users using btree (email_change_token_current)
where
(
(email_change_token_current)::text !~ '^[0-9 ]*$'::text
) tablespace pg_default;
create unique index email_change_token_new_idx on auth.users using btree (email_change_token_new)
where
(
(email_change_token_new)::text !~ '^[0-9 ]*$'::text
) tablespace pg_default;
create unique index reauthentication_token_idx on auth.users using btree (reauthentication_token)
where
(
(reauthentication_token)::text !~ '^[0-9 ]*$'::text
) tablespace pg_default;
create unique index recovery_token_idx on auth.users using btree (recovery_token)
where
((recovery_token)::text !~ '^[0-9 ]*$'::text) tablespace pg_default;
create unique index users_email_partial_key on auth.users using btree (email)
where
(is_sso_user = false) tablespace pg_default;
create index if not exists users_instance_id_email_idx on auth.users using btree (instance_id, lower((email)::text)) tablespace pg_default;
create index if not exists users_instance_id_idx on auth.users using btree (instance_id) tablespace pg_default;