Supabase (BaaS)

Supabase Authentication — Email, OAuth, SSO, MFA

Supabase Auth — email/password, magic link, OTP, 50+ OAuth providers, SSO, MFA, passkey, auth hooks.

Tổng quan

Supabase Auth cung cấp xác thực và phân quyền tích hợp sẵn. Dùng JSON Web Tokens (JWT) để xác thực, và tích hợp với Row Level Security (RLS) để phân quyền ở tầng database.

Kiến trúc Auth — 4 tầng

[Client SDK] --> [Kong API Gateway] --> [GoTrue Auth Service] --> [PostgreSQL]
     |                    |                       |                      |
     |              Rate limiting,          Xác thực user,         Lưu user data
     |              routing, API key        sinh JWT, gửi email    trong auth schema
     |              validation
  1. Client layer — Supabase client SDKs (@supabase/supabase-js, Flutter, Swift, Python, C#, Kotlin) hoặc HTTP request thủ công
  2. Kong API Gateway — Shared gateway cho tất cả Supabase products, xử lý rate limiting và routing
  3. Auth service (GoTrue) — Server xác thực, fork từ dự án GoTrue của Netlify, sinh JWT và quản lý session
  4. Postgres database — Lưu trữ user data trong auth schema (auth.users, auth.identities, auth.sessions, ...)
Auth schema không được expose qua Data API vì lý do bảo mật. Bạn có thể kết nối dữ liệu Auth với bảng của mình bằng foreign keys và triggers. Ví dụ: tạo bảng public.profiles với id UUID REFERENCES auth.users(id).

Cấu trúc JWT Token

Supabase Auth sử dụng JSON Web Token (JWT) làm cơ chế xác thực chính. Mỗi access token chứa các claims sau:

{
  "sub": "d0d4e701-5a5c-4c8d-b8e7-a1b2c3d4e5f6",
  "email": "user@example.com",
  "phone": "",
  "app_metadata": {
    "provider": "email",
    "providers": ["email", "google"]
  },
  "user_metadata": {
    "full_name": "Nguyen Van A",
    "avatar_url": "https://..."
  },
  "role": "authenticated",
  "aal": "aal1",
  "session_id": "abc123-def456-...",
  "iat": 1715702400,
  "exp": 1715706000
}

Giải thích từng claim trong JWT:

ClaimÝ nghĩaVí dụ
subUser UUID — định danh duy nhất của userd0d4e701-...
emailEmail của user (nếu dùng email auth)user@example.com
phoneSố điện thoại (nếu dùng phone auth)+84123456789
app_metadataMetadata từ provider, user không thể thay đổi{"provider": "email"}
user_metadataMetadata do app hoặc user set, có thể cập nhật{"full_name": "Vinh"}
rolePhân quyền cơ bản: authenticated hoặc anonauthenticated
aalAuthenticator Assurance Level: aal1 (chưa MFA) hoặc aal2 (đã MFA)aal2
session_idUUID của session hiện tạiabc123-...
iatIssued At — thời điểm token được tạo (Unix timestamp)1715702400
expExpiration — thời điểm token hết hạn (Unix timestamp)iat + 3600
Interview point: JWT claims rất quan trọng để hiểu cách Auth tích hợp với RLS. Khi gọi API, JWT được gửi trong Authorization: Bearer <token>. RLS policies dùng auth.uid() (lấy từ sub claim) và auth.role() (lấy từ role claim) để phân quyền. Custom claims có thể được thêm qua Custom Access Token Hook.

Token Lifetime — Thời gian sống của token

TokenThời gian mặc địnhCó thể cấu hình?Mục đích
Access Token (JWT)3,600 giây (1 giờ)Có — Dashboard > Authentication > SettingsXác thực mỗi API request
Refresh Token604,800 giây (7 ngày)Có — Dashboard > Authentication > SettingsLấy access token mới mà không cần login lại
Tại sao access token ngắn (1h) còn refresh token dài (7d)? Access token được gửi trong mỗi HTTP request — nếu bị đánh cắp, attacker chỉ có 1 giờ để khai thác. Refresh token chỉ được gửi đến Supabase Auth server khi cần refresh, ít rủi ro hơn. Nếu refresh token bị lộ, bạn có thể revoke nó trên server.

Refresh Flow — Cơ chế tự động refresh token

User Login thành công
  --> Server trả về access_token (JWT, hết hạn sau 1h)
                    + refresh_token (opaque string, hết hạn sau 7d)
  --> Client SDK lưu cả hai token trong localStorage / cookie

Mỗi API request:
  --> Client gửi access_token trong header Authorization: Bearer <access_token>

Khi access_token hết hạn (sau ~1h):
  --> Client SDK TỰ ĐỘNG gọi POST /auth/v1/token?grant_type=refresh_token
  --> Gửi refresh_token trong body
  --> Server kiểm tra refresh_token còn hợp lệ
  --> Server trả về access_token MỚI (JWT mới, 1h mới)
                    + có thể refresh_token mới (tùy cấu hình)
  --> Client cập nhật token mới, TIẾP TỤC gọi API
  --> USER KHÔNG HỀ BIẾT quá trình này xảy ra

Khi refresh_token hết hạn (sau 7d):
  --> Server từ chối refresh
  --> Client SDK phát hiện lỗi, throw error
  --> User phải login lại từ đầu
Interview point: Refresh flow là cơ chế quan trọng giúp user không phải login liên tục dù access token ngắn. Client SDK (như @supabase/supabase-js) tự động xử lý refresh — developer không cần viết logic refresh thủ công. Khi refresh token cũng hết hạn, onAuthStateChange fire event SIGNED_OUT.

Các phương thức xác thực

1. Email / Password

Phương thức cơ bản nhất — user đăng ký với email và mật khẩu:

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password',
  options: {
    data: {
      full_name: 'Nguyen Van A', // user metadata
    }
  }
})

// Email verification được gửi tự động nếu enable trong Dashboard
// Sau khi verify, user có thể sign in

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secure-password'
})

// data.session chứa access_token (JWT) và refresh_token
// data.user chứa user info (id, email, metadata)
Email verification: Bạn nên enable email verification trong Dashboard (Authentication > Settings). Khi bật, user phải verify email trước khi đăng nhập. Có thể tùy chỉnh email template và redirect URL.

User nhận link đăng nhập qua email — không cần nhớ mật khẩu:

const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://myapp.com/dashboard',
    shouldCreateUser: true, // Tự động tạo user nếu chưa tồn tại
  }
})

// User nhận email với magic link
// Click link sẽ redirect về app kèm session

Magic link an toàn hơn password vì:

  • Không cần user nhớ mật khẩu
  • Link chỉ dùng một lần và có thời hạn ngắn
  • Không có risk bị brute-force password

3. Phone Login (OTP qua SMS)

const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+84123456789',
  options: {
    channel: 'sms',
  }
})

// User nhận SMS chứa OTP 6 chữ số
// Verify OTP:
const { data, session, error } = await supabase.auth.verifyOtp({
  phone: '+84123456789',
  token: '123456',
  type: 'sms'
})

Phone Auth Providers — chi tiết:

ProviderĐặc điểmKhu vực hỗ trợ
MessageBirdGlobal coverage tốt, API đơn giảnToàn cầu (190+ quốc gia)
TwilioProvider phổ biến nhất, nhiều tính năngToàn cầu (180+ quốc gia)
Vonage (Nexmo)Giá cạnh tranh, chất lượng ổn địnhToàn cầu

Có thể cấu hình provider trong Dashboard > Authentication > Providers > Phone. Mỗi provider yêu cầu API key/secret riêng. Bạn cũng có thể tạo Send SMS Hook để dùng provider tùy chỉnh (như AWS SNS, Infobip).

WhatsApp OTP cũng được hỗ trợ thông qua Twilio — cấu hình channel là whatsapp.

4. Social Login (OAuth) — 50+ providers

// Google
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://myapp.com/auth/callback',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent',
    }
  }
})

// GitHub
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://myapp.com/auth/callback'
  }
})

Danh sách đầy đủ các OAuth provider được hỗ trợ:

ProviderLoạiGhi chú
AppleSocialHỗ trợ Sign in with Apple, iOS/macOS native
Azure (Microsoft)EnterpriseAzure AD / Microsoft Entra ID, hỗ trợ multi-tenant
BitbucketDeveloperDành cho team dùng Atlassian ecosystem
DiscordSocialPhổ biến cho game, community apps
FacebookSocialCần Facebook App ID + App Secret
FigmaDeveloperDành cho design/developer tools
GitHubDeveloperPhổ biến cho developer tools, SaaS
GitLabDeveloperSelf-managed GitLab instances cũng được hỗ trợ
GoogleSocialProvider phổ biến nhất, hỗ trợ Google Workspace domains
KakaoSocialPhổ biến ở Hàn Quốc (KakaoTalk)
KeycloakEnterpriseOpen-source identity provider, tự host
LinkedInSocialDành cho professional/recruitment apps
NotionProductivityDành cho apps tích hợp với Notion
SlackProductivityDành cho workspace/business apps
SpotifySocialDành cho music/audio apps
Twitter (X)SocialCần Twitter API v2 credentials
TwitchSocialDành cho gaming/streaming apps
WorkOSEnterpriseHỗ trợ nhiều enterprise IdPs qua một integration
ZoomProductivityDành cho video conferencing apps

Các provider khác: Auth0, AWS Cognito, Battle.net, Coinbase, Deezer, DigitalOcean, Discord, Dropbox, eBay, Etsy, Facebook (custom), Fitbit, Heroku, HubSpot, Instagram, Intercom, Kakao, Keycloak, Line, Linear, Mailchimp, Meetup, Naver, Notion, Okta, Patreon, Paypal, Pipedrive, Reddit, Salesforce, Shopify, Slack, SoundCloud, Spotify, Strava, Stripe, Tableau, TikTok, Todoist, Typeform, Uber, VK, X (Twitter), Yahoo, Yandex, YouTube, Zendesk, Zoom.

Interview point: Mỗi OAuth provider cần Client ID + Client Secret được cấu hình trong Dashboard. Redirect URL tự động là https://<project>.supabase.co/auth/v1/callback. Khi user login bằng OAuth, Supabase tự động tạo record trong auth.identities liên kết với auth.users. Một user có thể có nhiều identities (ví dụ: vừa Google vừa GitHub).

Bạn cũng có thể thêm Custom OAuth/OIDC Provider — cấu hình Client ID, Client Secret, Authorization URL, Token URL, và User Info URL.

5. Enterprise SSO (SAML 2.0)

Hỗ trợ Single Sign-On cho doanh nghiệp với SAML 2.0. Dùng cho các ứng dụng enterprise với identity provider như Okta, Azure AD, OneLogin.

// SSO sign in
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'company.com', // Email domain hoặc organization slug
  options: {
    redirectTo: 'https://myapp.com/dashboard'
  }
})

6. Passkey (WebAuthn) — Sinh trắc học

// Register passkey (Face ID, Touch ID, Windows Hello)
const { data } = await supabase.auth.registerPasskey()

// Sign in với passkey
const { data, error } = await supabase.auth.signInWithPasskey()

7. Anonymous Sign-Ins

Cho phép người dùng dùng app mà không cần đăng ký, sau đó có thể liên kết tài khoản:

// Anonymous sign in
const { data, error } = await supabase.auth.signInAnonymously()

// Sau đó, user có thể link với email/OAuth để trở thành permanent user
const { data, error } = await supabase.auth.linkIdentity({
  provider: 'google'
})

Identity Linking — quản lý nhiều phương thức đăng nhập cho cùng một user:

// Link Google identity vào tài khoản hiện tại
// (user đã login bằng email, muốn thêm Google login)
const { data, error } = await supabase.auth.linkIdentity({
  provider: 'google'
})

// Link GitHub identity
const { data, error } = await supabase.auth.linkIdentity({
  provider: 'github'
})

// Unlink — gỡ bỏ một identity khỏi tài khoản
// (chỉ hoạt động khi user còn ít nhất 1 identity khác)
const { data, error } = await supabase.auth.unlinkIdentity({
  identity_id: 'd0d4e701-5a5c-4c8d-b8e7-a1b2c3d4e5f6'
})

// Lấy danh sách tất cả identities của user hiện tại
const { data, error } = await supabase.auth.getUserIdentities()
// data.identities = [
//   { id: '...', provider: 'email', identity_data: { email: '...' } },
//   { id: '...', provider: 'google', identity_data: { email: '...' } },
// ]
Interview point — Identity Linking: Một user có thể link nhiều provider vào cùng một account (ví dụ: email + Google + GitHub). Khi user login bằng bất kỳ provider nào đã linked, họ đều vào cùng một account. getUserIdentities() trả về tất cả identities giúp UI hiển thị "Connected accounts". Khi unlink, user phải còn ít nhất 1 identity — không thể xóa identity cuối cùng.

8. Web3 Wallet (Ethereum / Solana)

const { data } = await supabase.auth.signInWithWeb3({
  provider: 'ethereum'
})

Hỗ trợ MetaMask, WalletConnect và các wallet Ethereum/Solana.


Session Management

Session được quản lý qua access token (JWT, thời hạn 1 giờ) và refresh token (dùng để lấy access token mới):

// Lấy session hiện tại
const { data: { session } } = await supabase.auth.getSession()

// Lấy user hiện tại
const { data: { user } } = await supabase.auth.getUser()

// Refresh session (access token hết hạn -> dùng refresh token lấy mới)
const { data, error } = await supabase.auth.refreshSession()

// Sign out
const { error } = await supabase.auth.signOut()

Theo dõi thay đổi Auth State

// Listen auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  switch (event) {
    case 'INITIAL_SESSION':
      console.log('Initial session loaded:', session)
      break
    case 'SIGNED_IN':
      console.log('User signed in:', session.user)
      break
    case 'SIGNED_OUT':
      console.log('User signed out')
      break
    case 'TOKEN_REFRESHED':
      console.log('Token refreshed')
      break
    case 'USER_UPDATED':
      console.log('User updated:', session.user)
      break
    case 'USER_DELETED':
      console.log('User deleted')
      break
    case 'PASSWORD_RECOVERY':
      console.log('Password recovery event')
      break
    case 'MFA_CHALLENGE_VERIFIED':
      console.log('MFA verified')
      break
  }
})

Giải thích từng onAuthStateChange event:

EventKhi nào fireHành động điển hình
INITIAL_SESSIONApp vừa load, session được khôi phục từ localStorageKhởi tạo UI dựa trên session có sẵn
SIGNED_INUser vừa sign in thành công (email, OAuth, magic link, OTP)Redirect về dashboard, fetch user data
SIGNED_OUTUser sign out hoặc refresh token hết hạnXóa local state, redirect về login
TOKEN_REFRESHEDAccess token được refresh tự độngCập nhật token trong app state (thường tự động)
USER_UPDATEDUser metadata thay đổi (updateUser, email change)Cập nhật UI với thông tin mới
USER_DELETEDUser bị xóa (từ admin API hoặc tự xóa)Xóa local data, redirect về trang chủ
PASSWORD_RECOVERYUser click link reset password từ emailHiển thị form đặt mật khẩu mới
MFA_CHALLENGE_VERIFIEDUser vượt qua MFA challenge thành côngNâng session lên aal2, mở khóa tính năng nhạy cảm
Interview point:onAuthStateChange là listener quan trọng nhất trong Supabase Auth. Dùng nó để:
  • Sync auth state với UI framework (Vue reactivity, React context)
  • Bảo vệ routes: SIGNED_OUT -> redirect /login, SIGNED_IN -> redirect /dashboard
  • Xử lý MFA flow: MFA_CHALLENGE_VERIFIED -> cập nhật UI
  • Cleanup: USER_DELETED -> xóa local data
  • INITIAL_SESSION giúp phân biệt "đang loading session" và "đã biết user chưa login"

Server-Side Auth — Xác thực trong API Routes

Khi xây dựng API với Next.js, Nuxt, SvelteKit, bạn cần xác thực user ở server side:

Next.js SSR Pattern

// app/api/protected/route.ts (Next.js App Router)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET() {
  const cookieStore = await cookies()
  const supabase = createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name) { return cookieStore.get(name)?.value },
        set(name, value, options) { cookieStore.set({ name, value, ...options }) },
        remove(name, options) { cookieStore.set({ name, value: '', ...options }) },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  return NextResponse.json({ user })
}

Nuxt SSR Pattern

// server/api/protected.get.ts (Nuxt 3)
import { serverSupabaseUser } from '#supabase/server'

export default defineEventHandler(async (event) => {
  const user = await serverSupabaseUser(event)
  if (!user) {
    throw createError({ statusCode: 401, message: 'Unauthorized' })
  }
  return { user }
})

SvelteKit SSR Pattern

// src/routes/api/protected/+server.ts (SvelteKit)
import { createServerClient } from '@supabase/ssr'
import type { RequestHandler } from './$types'

export const GET: RequestHandler = async ({ cookies }) => {
  const supabase = createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    { cookies: { /* ... */ } }
  )

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return new Response('Unauthorized', { status: 401 })
  return new Response(JSON.stringify({ user }))
}

Admin Operations trên Server

// Dùng service role key cho admin operations (backend only)
const supabaseAdmin = createClient(url, serviceRoleKey)

// Verify user từ JWT gửi từ client
const { data: user, error } = await supabaseAdmin.auth.admin.getUser(jwt)

// Decode JWT không verify (chỉ đọc claims, không kiểm tra chữ ký)
const claims = supabaseAdmin.auth.getClaims(jwt)
// { sub: '...', email: '...', role: 'authenticated', ... }

// List tất cả users với pagination
const { data: { users, total } } = await supabaseAdmin.auth.admin.listUsers({
  page: 1,
  perPage: 100
})
Quan trọng: Service role key bypass RLS và có quyền admin toàn bộ. Tuyệt đối không expose service role key ra client. Chỉ dùng trong Edge Functions, API routes server-side, hoặc backend. Kiểm tra Authorization header và verify JWT trước khi thực hiện admin operations.

Multi-Factor Authentication (MFA)

Hỗ trợ MFA với TOTP (Time-based One-Time Password)Phone:

// Bắt đầu enroll MFA (TOTP)
const { data, error } = await supabase.auth.mfa.enroll({
  factorType: 'totp',
  issuer: 'My App',
  friendlyName: 'user@example.com'
})

// Trả về QR code URL để user scan với Authenticator app
const { qr_code } = data.totp

// Challenge sau khi user đã enroll
const { data, error } = await supabase.auth.mfa.challenge({
  factorId: factorId
})

// Verify TOTP code
const { data, error } = await supabase.auth.mfa.verify({
  factorId: factorId,
  code: '123456', // 6-digit code từ Authenticator app
  challengeId: challengeId
})

// Unenroll
const { data, error } = await supabase.auth.mfa.unenroll({
  factorId: factorId
})

// Lấy danh sách các factor đã enroll
const { data } = await supabase.auth.mfa.listFactors()

MFA Flow

  1. User login với email/password
  2. Server trả về aal1 session (chưa MFA)
  3. Client gọi mfa.challenge() với factorId
  4. User nhập TOTP code từ Authenticator app
  5. Client gọi mfa.verify() để xác nhận
  6. Server trả về aal2 session (đã MFA, full access)

Auth Hooks

Customize hành vi Auth tại các điểm lifecycle bằng Postgres functions:

HookMục đíchKhi nào dùng
Custom Access TokenThêm claims vào JWT access tokenThêm role, permissions, metadata vào JWT
Send SMSTùy chỉnh gửi SMSDùng provider SMS tùy chỉnh thay vì MessageBird/Twilio
Send EmailTùy chỉnh gửi emailDùng Resend, SendGrid, custom SMTP thay vì Supabase default
MFA Verification AttemptCustom logic khi verify MFARate limiting, logging, custom validation
-- Custom Access Token Hook: thêm custom claims vào JWT
-- Hook này được gọi mỗi khi JWT được tạo hoặc refresh
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
DECLARE
  claims jsonb := event->'claims';
  user_data jsonb;
BEGIN
  -- Lấy thông tin từ bảng profiles
  SELECT jsonb_build_object(
    'plan', plan,
    'role', role,
    'tenant_id', tenant_id
  )
  INTO user_data
  FROM public.profiles
  WHERE user_id = (event->>'user_id')::uuid;

  -- Merge thông tin vào claims
  -- user_metadata sẽ xuất hiện trong JWT và có thể dùng trong RLS policy
  claims := claims || jsonb_build_object('user_metadata', user_data);

  -- Set lại claims trong event
  RETURN jsonb_set(event, '{claims}', claims);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Sau đó enable hook trong Dashboard > Authentication > Hooks
-- Chọn "Custom Access Token" và chọn function public.custom_access_token_hook
-- Send Email Hook: tùy chỉnh gửi email qua Resend/SendGrid
CREATE OR REPLACE FUNCTION public.custom_send_email_hook(event jsonb)
RETURNS jsonb AS $$
DECLARE
  response jsonb;
BEGIN
  -- Gọi external email API (Resend, SendGrid, v.v.)
  SELECT content::jsonb INTO response
  FROM http((
    'POST',
    'https://api.resend.com/emails',
    ARRAY[http_header('Authorization', 'Bearer re_xxxxx')],
    'application/json',
    jsonb_build_object(
      'from', 'My App <noreply@myapp.com>',
      'to', ARRAY[event->>'email'],
      'subject', event->>'email_action_type',
      'html', event->>'redirect_to'
    )::text
  ));

  RETURN event;
END;
$$ LANGUAGE plpgsql;
Interview point — Custom Access Token Hook: Đây là cách để thêm custom claims (như plan, role, tenant_id) vào JWT mà không cần tự tạo JWT server riêng. Claims được thêm vào sẽ có sẵn trong RLS policies qua auth.jwt(). Hook chạy trong Postgres với SECURITY DEFINER để có quyền đọc bảng profiles.

Auth UI Components

Supabase cung cấp pre-built UI components:

npm install @supabase/supabase-js @supabase/ssr

# Auth UI (React)
npm install @supabase/auth-ui-react @supabase/auth-ui-shared

# Auth UI (Vue)
npm install @supabase/auth-ui-vue
<!-- Vue example -->
<script setup>
import { Auth } from '@supabase/auth-ui-vue'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, anonKey)
</script>

<template>
  <Auth :supabase-client="supabase" />
</template>

Component <Auth /> tự động bao gồm: Sign Up, Sign In, Magic Link, OAuth buttons, password reset, update password.


User Management (Admin)

Dùng service role key để quản lý users từ backend:

// Khởi tạo admin client (backend only)
const supabaseAdmin = createClient(url, serviceRoleKey)

// Lấy user theo ID
const { data, error } = await supabaseAdmin.auth.admin.getUserById(userId)

// Liệt kê tất cả users
const { data, error } = await supabaseAdmin.auth.admin.listUsers({
  page: 1,
  perPage: 100
})

// Tạo user (không gửi email xác nhận)
const { data, error } = await supabaseAdmin.auth.admin.createUser({
  email: 'user@example.com',
  password: 'password123',
  email_confirm: true, // Bypass email verification
  user_metadata: { full_name: 'Nguyen Van A' }
})

// Cập nhật user
const { data, error } = await supabaseAdmin.auth.admin.updateUserById(userId, {
  email: 'newemail@example.com',
  user_metadata: { role: 'admin' }
})

// Xóa user
const { data, error } = await supabaseAdmin.auth.admin.deleteUser(userId)

Pricing Auth

Tính phí theo Monthly Active Users (MAU):

PlanMAUGiá vượt
Free50,000 MAU
Pro100,000 MAU$0.00325/MAU
  • Third-Party MAU (OAuth, phone auth): tính phí riêng
  • SSO MAU (SAML): tính phí riêng
  • Advanced MFA (Phone): add-on tính phí

Password Security — Chi tiết

Rate Limits

Supabase áp dụng rate limiting để chống brute-force attacks:

Hành độngGiới hạnTime window
Login attempts (email/password)5 lầnMỗi phút, mỗi email
Signup attempts5 lầnMỗi giờ, mỗi email
OTP requests (email/phone)5 lầnMỗi phút, mỗi email/phone
Password reset requests5 lầnMỗi giờ, mỗi email
Token refreshKhông giới hạn cứngRate limit nhẹ
Overall auth requests30 req/sMỗi IP
Các rate limit này là hard limits trên server — không thể bypass bằng client code. Khi vượt quá, API trả về 429 Too Many Requests. Đối với Pro/Team plan, có thể liên hệ support để điều chỉnh.

Password Requirements

  • Độ dài tối thiểu: 6 ký tự (mặc định)
  • Không có yêu cầu về uppercase, lowercase, số, hoặc ký tự đặc biệt theo mặc định
  • Có thể tăng requirements qua Custom Auth Hook hoặc validate ở client trước khi gửi
  • Mật khẩu được hash bằng bcrypt/scrypt (không lưu plaintext)
  • Có thể cấu hình password policy chi tiết hơn trong Dashboard > Authentication > Settings

Bot Detection (CAPTCHA)

CAPTCHA ProviderĐặc điểmCấu hình trong Dashboard
hCaptchaCần site key + secret key, hoạt động như reCAPTCHAAuthentication > Settings > CAPTCHA
Cloudflare TurnstileMiễn phí, bảo vệ privacy, không tracking userAuthentication > Settings > CAPTCHA
// Khi enable CAPTCHA, client SDK tự động lấy token
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123',
  options: {
    captchaToken: token // Từ Turnstile/hCaptcha widget
  }
})
CAPTCHA được tự động kích hoạt khi Supabase phát hiện dấu hiệu bot dựa trên heuristics. Bạn cũng có thể bắt buộc CAPTCHA cho tất cả sign up để tăng bảo mật.

Audit Logs

-- Xem audit logs (Team plan trở lên)
SELECT
  created_at,
  ip_address,
  payload->>'action' as action,
  payload->>'actor_username' as user_email,
  payload->>'traits' as details
FROM auth.audit_log_entries
WHERE created_at > now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 100;

-- Các action được log:
-- 'user_signedup', 'user_confirmed', 'user_repeated_signup',
-- 'login', 'token_refreshed', 'user_modified',
-- 'user_deleted', 'user_recovery_requested'

⭐ Interview Callouts

Interview tip — Auth + RLS Integration: Supabase Auth dùng JWT. Khi user login, JWT được lưu trong localStorage. Khi gọi API, JWT tự động được gửi kèm. RLS policies dùng auth.uid() để lấy user ID từ JWT — đây là cách Auth và Database tích hợp với nhau. Luôn bật RLS trước khi expose dữ liệu.
Interview tip — JWT Claims: JWT chứa sub (user ID), role (authenticated/anon), và email. Bạn có thể thêm custom claims qua Custom Access Token Hook. Khi token hết hạn, refresh token được dùng để lấy access token mới. Service role key bypass RLS — chỉ dùng trong Edge Functions hoặc backend.
Interview tip — Magic Link vs Password: Magic Link an toàn hơn password vì: (1) không cần user tạo/nhớ password, (2) link chỉ dùng một lần và hết hạn nhanh, (3) không có risk brute-force. Tuy nhiên, user cần access email mỗi lần login — không tiện bằng password với "remember me".
Interview tip — Identity Linking: Một user có thể có nhiều identity (email, Google, GitHub, ...). Dùng linkIdentity() để thêm provider, unlinkIdentity() để gỡ, getUserIdentities() để xem danh sách. Khi login bằng bất kỳ identity nào đã link, user vào cùng một account. Không thể unlink identity cuối cùng.
Interview tip — Refresh Flow: Access token (JWT) sống 1 giờ, refresh token sống 7 ngày. Client SDK tự động refresh access token khi hết hạn — developer không cần code logic này. Khi cả hai đều hết hạn, user phải login lại. Đây là balance giữa security (token ngắn) và UX (không login thường xuyên).
Interview tip — Server-Side Auth: Khi dùng Next.js/Nuxt/SvelteKit SSR, dùng createServerClient từ @supabase/ssr để verify user ở server. Service role key cho admin operations — tuyệt đối không expose ra client. Pattern phổ biến: client gửi JWT trong Authorization header -> server dùng supabase.auth.admin.getUser(jwt) để verify.

Bài tập

Bài tập 4: Full Auth Flow — Đăng ký, xác minh, đăng nhập, quản lý session
  1. Setup Supabase project với email verification bắt buộc
  2. Tạo Nuxt/Vue app với trang Sign Up (signUp), Sign In (signInWithPassword), Dashboard
  3. Implement flow:
    • User đăng ký -> nhận email verification
    • Verify email -> redirect về app
    • User đăng nhập -> nhận JWT session
    • Dashboard hiển thị user info từ getUser()
    • Auto-refresh session khi access token hết hạn
    • Sign out và redirect về trang login
  4. Theo dõi onAuthStateChange để sync UI với auth state
  5. Middleware bảo vệ route: redirect về login nếu chưa đăng nhập
Bài tập 5: Google OAuth + Magic Link
  1. Cấu hình Google OAuth provider trong Supabase Dashboard
  2. Tạo nút "Sign in with Google" trong app
  3. Implement Magic Link như alternative login method
  4. Sau khi OAuth login, tạo record trong bảng public.profiles (dùng Database Trigger)
  5. Xử lý case: user dùng Google OAuth và Magic Link với cùng một email
  6. Bonus: Thêm GitHub OAuth provider
Bài tập 6: MFA với TOTP
  1. Enable MFA trong Supabase project
  2. Tạo flow enroll MFA: user scan QR code với Google Authenticator
  3. Tạo flow challenge: sau khi login password, yêu cầu TOTP code
  4. Verify TOTP code và lấy aal2 session
  5. Xử lý recovery codes khi user mất Authenticator app
  6. Bonus: Gửi SMS OTP như MFA factor thứ hai
Bài tập 7: Custom Access Token Hook
  1. Tạo bảng public.roles với user_idrole_name (admin, editor, viewer)
  2. Viết custom_access_token_hook Postgres function để thêm role vào JWT claims
  3. Enable hook trong Dashboard
  4. Viết RLS policy dùng claim từ JWT: admin được edit tất cả posts, editor được edit posts của họ, viewer chỉ được read
  5. Test: login với user có role khác nhau, kiểm tra quyền truy cập

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.