Supabase OAuth 2.1은 로그인 기능이 아니라 위임 권한 계약이다
내 Supabase 앱이 다른 앱의 “로그인 제공자”가 될 때, 데이터 권한은 어디서 끊어야 할까?
Supabase OAuth 2.1은 로그인 기능이 아니라 위임 권한 계약이다
- 카테고리: web_app_dev
- 예상 읽기 시간: 10분
- 오늘의 질문: 내 Supabase 앱이 다른 앱의 “로그인 제공자”가 될 때, 데이터 권한은 어디서 끊어야 할까?
- 핵심 출처:
- Build “Sign in with Your App” using Supabase Auth - 공식 블로그, 확인일 2026-06-21
- OAuth 2.1 Server - Supabase Docs, 확인일 2026-06-21
- Token Security and Row Level Security - Supabase Docs, 확인일 2026-06-21
- Custom OAuth/OIDC Providers - Supabase Docs, 확인일 2026-06-21
- RFC 9700: Best Current Practice for OAuth 2.0 Security - IETF, 2025-01, 확인일 2026-06-21
1. 왜 지금 봐야 하나
Supabase Auth를 쓰는 작은 웹앱 팀은 보통 “우리 서비스에 로그인시키는 기능”부터 만든다. 이메일, GitHub, Google, 패스키, 세션 갱신, RLS 정도를 붙이면 MVP는 돌아간다. 그런데 최근 Supabase Auth의 방향은 한 단계 더 나아갔다. Supabase 프로젝트가 단순히 로그인 소비자가 아니라 OAuth 2.1 / OpenID Connect identity provider로 동작할 수 있게 되었다. 즉 다른 앱이 “Sign in with Your App” 버튼을 만들고, 사용자는 내 서비스 계정으로 그 앱에 로그인할 수 있다.
이 변화가 중요한 이유는 “로그인 버튼 하나 더”가 아니기 때문이다. 내 서비스가 identity provider가 되면, 외부 클라이언트가 사용자를 대신해 내 API와 DB에 접근할 수 있다. AI 에이전트, MCP 클라이언트, 파트너 대시보드, 모바일 앱, 기업 SSO가 모두 같은 사용자 계정 뒤에 붙는다. 이때 제품 사고는 이렇게 바뀐다.
“이 사용자가 누구인가?”에서 “이 사용자를 대신해 어떤 클라이언트가, 어떤 범위로, 어떤 데이터를 만지는가?”로 바뀐다.
Supabase 문서는 OAuth 2.1 Server가 authorization code flow with PKCE를 구현하고, access token에 user_id, role, client_id 같은 claim을 넣으며, 기존 RLS 정책과 함께 쓸 수 있다고 설명한다. 동시에 Token Security 문서는 중요한 경고를 한다. OAuth scope(openid, email, profile, phone)는 ID token과 UserInfo endpoint에 들어갈 사용자 정보 범위를 제어할 뿐, 데이터베이스 테이블 접근 권한을 대신하지 않는다. DB 접근은 RLS로 막아야 한다.
오늘의 핵심 문장은 이것이다. Supabase OAuth 2.1은 인증 기능이 아니라, 사용자 권한을 외부 클라이언트에 위임할 때 RLS와 consent UI로 끊어야 하는 권한 계약이다.
2. 핵심 개념
OAuth를 “로그인”으로만 이해하면 사고가 난다. OAuth의 본질은 사용자가 어떤 클라이언트에게 제한된 접근을 위임하는 흐름이다. OIDC는 그 위에 “이 사용자가 누구인지”를 표준 ID token으로 표현하는 identity layer다.
Supabase OAuth 2.1 Server 흐름을 개발자 언어로 풀면 다음과 같다.
- 외부 앱이 사용자를 내 Supabase 프로젝트의 authorization endpoint로 보낸다.
- Supabase Auth가 요청을 검증하고, 내가 만든 authorization UI로 사용자를 보낸다.
- 사용자는 기존 Supabase Auth 방식으로 로그인한다. 비밀번호, 매직 링크, 소셜 로그인, MFA 같은 기존 설정이 그대로 쓰인다.
- 사용자가 외부 앱의 접근 요청을 승인하거나 거절한다.
- 승인되면 authorization code가 발급된다.
- 외부 앱은 code와 PKCE verifier를 token endpoint에 제출해 access token과 refresh token을 받는다.
- 외부 앱은 access token으로 내 API 또는 Supabase API에 접근한다.
여기서 PKCE는 “code를 훔쳐도 token으로 바꾸기 어렵게 만드는 장치”다. IETF RFC 9700은 public client는 PKCE를 반드시 써야 하고, confidential client도 PKCE 사용을 권장한다고 정리한다. OAuth 2.1 초안과 현재 보안 관행은 implicit flow나 resource owner password credentials 같은 오래된 흐름을 밀어내고, authorization code + PKCE 쪽으로 수렴한다.
하지만 PKCE는 token 발급 과정의 안전장치일 뿐이다. token이 발급된 뒤 “그 token으로 어떤 row를 읽고 쓸 수 있는가”는 별도 문제다. Supabase에서 이 경계는 RLS다.
Supabase OAuth access token에는 일반 Supabase JWT claim에 더해 client_id가 들어간다. 이 값은 “사용자가 로그인했다”가 아니라 “어떤 OAuth 클라이언트가 이 token을 받았는가”를 알려준다. 그래서 RLS에서는 최소한 두 축을 함께 봐야 한다.
-- 이 row가 현재 사용자 소유인가?
auth.uid() = user_id
-- 이 요청이 허용된 OAuth client에서 왔는가?
(auth.jwt() ->> 'client_id') = 'partner-dashboard-client-id'
사용자 소유권만 보면 외부 앱이 사용자의 모든 데이터를 가져갈 수 있다. client_id만 보면 다른 사용자의 데이터가 섞일 수 있다. 둘을 함께 봐야 위임 권한이 된다.
3. 최신 이슈와 연결
이번 Supabase 흐름에는 양방향 변화가 있다.
첫째, Supabase 프로젝트가 identity provider가 될 수 있다. 공식 블로그는 Supabase Auth가 OAuth 2.1과 OIDC server capability를 제공해 “Sign in with Your App”, 개발자 플랫폼, 파트너 통합, enterprise SSO, MCP 기반 AI agent 인증을 만들 수 있다고 설명한다. OAuth discovery, OIDC discovery, JWKS endpoint, Dynamic Client Registration 같은 표준 구성 요소도 언급된다.
둘째, Supabase 프로젝트가 임의의 외부 identity provider를 소비할 수도 있다. Custom OAuth/OIDC Providers 문서는 built-in provider 목록에 없는 OIDC/OAuth2 제공자를 붙이는 방법을 설명한다. OIDC 방식은 issuer URL만 주면 discovery document, authorization endpoint, token endpoint, UserInfo endpoint, JWKS URI를 자동으로 해석한다. custom provider identifier는 custom: 접두사를 사용한다.
이 두 변화가 합쳐지면 Supabase Auth는 단순 “소셜 로그인 어댑터”가 아니라 identity hub에 가까워진다.
- 내 앱이 외부 IdP를 소비한다: 회사 SSO, 지역 인증, self-hosted GitHub/GitLab, 산업별 IdP를 Supabase 로그인으로 받아들인다.
- 내 앱이 다른 앱의 IdP가 된다: 파트너 앱, AI agent, MCP client, 사내 도구가 내 Supabase 계정으로 사용자 인증을 한다.
- DB 접근은 여전히 Postgres/RLS가 결정한다: scope 이름만 보고 “profile scope니까 profiles table SELECT 허용” 같은 식으로 구현하면 안 된다.
특히 AI agent와 MCP 맥락에서는 이 구분이 더 중요하다. 에이전트는 사람보다 많은 요청을 빠르게 만들 수 있고, 사용자는 “내 계정으로 연결” 버튼 하나만 보고 승인할 수 있다. 그래서 consent UI와 RLS는 에이전트 시대의 제품 안전장치가 된다.
4. 개발자 관점 해석
작은 웹앱 팀이 여기서 가져가야 할 관점은 세 가지다.
4-1. scope는 UX 언어, RLS는 데이터 권한 언어다
OAuth scope는 사용자에게 “이 앱이 이메일, 프로필, 전화번호 같은 identity 정보를 요청한다”를 설명하는 데 유용하다. 하지만 Supabase 문서가 명시하듯 scope는 DB table 접근을 제어하지 않는다. profile scope가 있다고 해서 profiles table의 모든 row를 읽어도 된다는 뜻이 아니다.
따라서 제품 설계 문서에는 scope와 RLS를 따로 써야 한다.
| 항목 | 질문 | 예시 |
|---|---|---|
| OAuth scope | 사용자에게 어떤 identity 정보를 보여줄 것인가? | openid email profile |
| consent UI | 사용자가 어떤 앱에 무엇을 허용한다고 이해해야 하는가? | “Acme Dashboard가 내 공개 프로필과 월별 통계 요약을 읽습니다” |
| RLS policy | token이 실제로 어떤 table/row/action에 접근 가능한가? | user_metrics SELECT only + client_id 제한 |
4-2. client_id는 권한 모델의 1급 입력이다
기존 Supabase 앱에서는 RLS가 보통 auth.uid() = user_id로 시작한다. OAuth client가 붙으면 이 조건만으로 부족하다. 같은 사용자라도 직접 웹앱에서 로그인한 세션, 공식 모바일 앱, 파트너 분석 도구, AI 에이전트는 권한이 달라야 한다.
예를 들어 민감한 결제 정보는 직접 사용자 세션에서만 보이게 하고, OAuth client token에서는 막을 수 있다.
CREATE POLICY "OAuth clients cannot access payment info"
ON payment_methods FOR ALL
USING (
auth.uid() = user_id
AND (auth.jwt() ->> 'client_id') IS NULL
);
반대로 파트너 대시보드는 집계 데이터만 읽게 할 수 있다.
CREATE POLICY "Partner dashboard reads monthly summaries"
ON monthly_usage_summaries FOR SELECT
USING (
auth.uid() = user_id
AND (auth.jwt() ->> 'client_id') = 'partner-dashboard-client-id'
);
핵심은 “사용자 소유권 + 클라이언트 정체성 + 작업 종류”를 같이 본다는 점이다.
4-3. authorization UI는 보안 UI다
OAuth Server를 켜면 사용자를 내 authorization UI로 보내 승인/거절을 처리해야 한다. 이 화면은 단순 confirm dialog가 아니다. 사용자가 어떤 앱에 무엇을 위임하는지 이해하는 마지막 경계다.
나쁜 consent UI:
“이 앱에 접근을 허용하시겠습니까?”
좋은 consent UI:
“Partner Analytics가 내 계정의 월별 사용량 요약을 읽습니다. 원본 주문 내역, 결제 수단, 비공개 메모는 접근할 수 없습니다.”
좋은 UI는 RLS 정책과 같은 말을 해야 한다. UI는 “월별 요약만 읽는다”고 말하는데 RLS가 user_data FOR ALL을 허용하면 보안 설명이 거짓이 된다. 반대로 RLS는 제한적인데 UI가 모호하면 사용자는 불필요하게 불안해진다.
5. 내 프로젝트에 적용할 체크포인트
아래 체크리스트는 Supabase OAuth 2.1 Server를 당장 켜지 않더라도 인증/인가 설계 리뷰에 쓸 수 있다.
OAuth client 등록 전
- 이 client는 first-party인가, partner인가, public third-party인가?
- redirect URI는 정확히 고정되어 있는가? wildcard나 open redirect를 기대하지 않는가?
- public client라면 PKCE가 전제되어 있는가?
- token을 받을 client가 서버를 가진 confidential client인지, 브라우저/모바일 public client인지 구분했는가?
- refresh token을 장기 보관해야 한다면 회수/재발급/로그아웃 정책이 있는가?
consent UI 작성 전
- client 이름, 운영 주체, redirect 대상이 사용자에게 보이는가?
- 사용자가 승인하는 데이터 범위를 사람이 이해하는 문장으로 썼는가?
- “읽기”와 “쓰기”를 분리해 설명하는가?
- AI agent나 자동화 도구라면 사용자가 나중에 연결을 끊을 수 있는가?
RLS 작성 전
- 모든 OAuth 접근 정책이
auth.uid() = user_id같은 사용자 소유권 조건을 포함하는가? - OAuth client별로
auth.jwt() ->> 'client_id'를 확인하는가? - 민감 테이블에는 OAuth client 전체 차단 또는 제한 정책이 있는가?
SELECT와INSERT/UPDATE/DELETE권한을 따로 검토했는가?- 기존 direct user session 정책과 OAuth client 정책이 permissive OR 결합으로 과도하게 열리지 않는가?
- 필요하면 restrictive policy로 추가 제약을 걸었는가?
Custom OAuth/OIDC provider를 붙일 때
- OIDC provider라면 issuer와 discovery document를 확인했는가?
- JWKS 검증과 ID token audience 검증 조건을 이해했는가?
- provider identifier를
custom:접두사로 관리하고 환경별 이름 충돌을 피했는가? - 이메일이 없는 provider라면
email_optional을 켤 때 계정 식별/복구 UX를 따로 설계했는가? - multi-platform app이면
acceptable_client_ids같은 audience 검증 예외를 명확히 문서화했는가?
6. 오늘 10분 액션
오늘은 코드를 많이 쓰지 말고 “권한표”를 그려보자.
- 내 서비스에 붙을 수 있는 client 3개를 상상한다.
- 공식 웹앱
- 공식 모바일 앱
- 파트너/AI 에이전트/분석 도구 중 하나
- 각 client에 대해 아래 표를 채운다.
| client | user identity 필요 | 읽을 수 있는 테이블 | 쓸 수 있는 테이블 | 절대 금지 데이터 | consent 문장 |
|---|---|---|---|---|---|
| web app | yes | profiles, orders | profiles | payment_methods raw | 내 계정 관리 |
| mobile app | yes | profiles, notifications | device_tokens | admin_notes | 모바일 알림과 프로필 |
| partner analytics | yes | monthly_usage_summaries | 없음 | orders, payment_methods | 월별 사용량 요약 읽기 |
- partner analytics에 대해 RLS 조건을 한 줄로 적는다.
auth.uid() = user_id
AND (auth.jwt() ->> 'client_id') = 'partner-analytics-client-id'
- consent UI 문장과 RLS 조건이 같은 약속을 하는지 비교한다.
이 10분 액션의 목표는 OAuth를 완전히 구현하는 것이 아니다. “로그인 성공”과 “데이터 접근 허용”을 분리해서 생각하는 습관을 만드는 것이다.
7. 더 볼 자료
- Supabase OAuth 2.1 Server: Supabase 프로젝트를 OAuth/OIDC provider로 만드는 전체 개요.
- Supabase OAuth Token Security and RLS: OAuth scope와 RLS의 역할 차이,
client_idclaim 사용 패턴. - Supabase Custom OAuth/OIDC Providers: 외부 OIDC/OAuth2 provider를 Supabase 로그인에 붙이는 방법.
- RFC 9700: OAuth 2.0 보안 최신 권고. PKCE, redirect URI, CSRF, mix-up attack 같은 실무 위협 모델을 확인하기 좋다.
중복 회피 메모
로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 WebMCP 브라우저 도구 계약, View Transition 상태 변화 계약, Supabase 브랜칭/pg-delta, agent-friendly content negotiation, React Compiler, Supabase AI 에이전트 도구 권한표, Next.js Cache Components, Supabase Auth 이메일/패스키/RLS Tester/Realtime을 다뤘다. 이번 글은 Supabase를 다시 다루지만 이메일 발신, 패스키, RLS 테스트, DB 브랜칭, AI 도구 권한표가 아니라 Supabase Auth가 OAuth 2.1/OIDC provider 또는 custom OIDC consumer가 될 때 scope, consent UI, client_id claim, RLS를 어떻게 분리해 위임 권한 계약으로 설계할지에 집중했다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)