Skip to content

Supabase:Auth

Managing User Data

Server-Side Rendering

사용자가 Supabase Auth로 인증하면 서버에서 두 가지 정보가 발행됩니다.

  • Access TokenJWT 형식 입니다.
  • Refresh Token 은 랜덤으로 생성된 문자열 입니다.

대부분의 Supabase 프로젝트는 <project-ref>.supabase.co/auth/v1 에서 Listening 중인 인증 서버가 있다.

따라서 액세스 토큰 및 새로 고침 토큰은 <project-ref>.supabase.co 도메인으로 sb-access-tokensb-refresh-token 쿠키로 설정됩니다.

WARNING

이러한 쿠키 이름은 Supabase 전용으로 사용되며, 경고 없이 변경될 수 있습니다. 설명 목적으로만 이용해야 한다.

웹 브라우저는 SOP (Same-Origin Policy) 에 따라 도메인 전체에서 쿠키에 대한 액세스를 제한합니다. 귀하의 웹 애플리케이션은 이러한 쿠키에 액세스할 수 없으며 이러한 쿠키는 귀하의 애플리케이션 서버로 전송되지 않습니다.

인증 흐름 이해

signIn 메서드 중 하나를 호출하면, 브라우저에서 실행 중인 클라이언트 라이브러리가 Supabase Auth 서버로 요청을 보냅니다.

인증 서버는 전화번호, 이메일 및 비밀번호 조합, Magic Link 또는 소셜 로그인(프로젝트에 설정이 있는 경우)을 사용할지 여부를 결정합니다.

사용자 ID가 성공적으로 확인되면 Supabase Auth 서버는 사용자를 다시 SPA (Single-page app) 으로 리디렉션 합니다.

Tip

Supabase 대시보드에서 리디렉션 URL을 구성할 수 있습니다. ***와 같은 wildcard match patterns을 사용하여 다른 형식의 URL로도 리디렉션할 수 있습니다.

Supabase Auth는 암시적 (Implicit)#PKCE Flow의 두 가지 인증 흐름을 지원합니다.

#PKCE Flow 흐름은 일반적으로 서버에 있을 때 선호됩니다. Replay attackURL capture 공격으로부터 보호하는 몇 가지 추가 단계를 소개합니다.

"암시적 흐름 (Implicit flow)"와 달리 사용자가 서버의 access_tokenrefresh_token에 액세스할 수도 있습니다.

Implicit Flow

암시적 흐름을 사용하는 경우 다음 구조로 리디렉션 URL이 반환됩니다.

https://yourapp.com/...#access_token=<...>&refresh_token=<...>&...

성공적인 확인 후 첫 번째 액세스 및 새로 고침 토큰은 리디렉션 위치의 URL Fragment(# 기호 뒤의 모든 항목)에 포함됩니다. 이것은 의도적이며 수정할 수 없습니다.

클라이언트 라이브러리는 이러한 유형의 URL을 수신하고 여기에서 "액세스 토큰" 과 "새로 고침 토큰" 및 일부 추가 정보를 추출하고 마지막으로 라이브러리 및 앱에서 추가로 사용할 수 있도록 로컬 스토리지에 유지하도록 설계되었습니다.

상세 정보
웹 브라우저는 요청을 보내는 서버에 URL 조각을 보내지 않습니다.
귀하가 직접 제어하는 ​​서버(예: GitHub Pages 또는 기타 부분 유료화 호스팅 공급자)에서 "SPA (단일 페이지 앱)"을 호스팅하지 않을 수 있으므로 기본적으로 호스팅 서비스가 사용자의 인증 자격 증명에 액세스하지 못하도록 합니다.
서버가 직접 제어되는 경우에도 GET 요청과 해당 전체 URL이 기록되는 경우가 많습니다.
이 접근 방식은 또한 요청 또는 액세스 로그에서 자격 증명 유출을 방지합니다. 서버에서 access_tokenrefresh_token를 얻으려면 #PKCE Flow 을 사용하는 것이 좋습니다.

PKCE Flow

PKCE 흐름을 사용하는 경우 다음 구조로 리디렉션 URL이 반환됩니다.

https://yourapp.com/...?code=<...>

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

세분화된 권한 부여 규칙이 필요한 경우 PostgreSQL의 RLS (Row Level Security)를 능가하는 것은 없습니다 .

정책은 PostgreSQL의 규칙 엔진입니다. 매우 강력하고 유연하여 고유한 비즈니스 요구에 맞는 복잡한 SQL 규칙을 작성할 수 있습니다.

RLS는 다음과 같이 활성화 할 수 있습니다:

alter table public.profiles enable row level security;

Policies

정책은 요령을 터득하면 쉽게 이해할 수 있습니다. 각 정책은 테이블에 연결되며 테이블에 액세스할 때마다 정책이 실행됩니다.

모든 쿼리에 WHERE절을 추가하는 것으로 생각할 수 있습니다.

예를 들어 다음과 같은 정책이 있을 때:

create policy "Individuals can view their own todos."
    on todos for select
    using ( auth.uid() = user_id );

사용자가 todos 테이블에서 선택하려고 할 때마다 다음과 같이 변환됩니다:

select *
from todos
where auth.uid() = todos.user_id; -- Policy is implicitly added.

이 패턴의 좋은 점은? 이제 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 열에 저장됩니다. 메타데이터를 보려면:

const {
  data: { user },
} = await supabase.auth.getUser()
let metadata = user.user_metadata

새로운 사용자 이벤트 트리거

사용자가 가입할 때마다 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 수정:

const result = await supabase.auth.updateUser({data: {nickname: 'user-name'}});

삭제:

const result = await supabase.auth.updateUser({data: {profile: null}});

INFORMATION

저렇게 명시적으로 추가된 내용이 아닌 Json 프로퍼티 들은 변경되지 않는다. 그러니 국민 여러분 안심하십시오

OAuth : Identity linking strategies

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;

See also

Favorite site