Relyt One 관리형 인증, 미리 보기 공개

앱 사용자를 위한 가입, 로그인, 세션, 조직 기능이 브랜치 바로 옆에서 실행되고 사용자는 여러분의 데이터베이스에 저장됩니다. 토큰은 JWKS로 검증하는 수명이 짧은 EdDSA JWT이며, 나머지는 행 수준 보안이 처리합니다.

Zhan Lu2026년 10월 30일 업데이트읽는 시간 6분
이 페이지의 내용

이제 Relyt One에서 관리형 인증을 미리 보기로 사용할 수 있습니다. 브랜치에서 관리형 인증을 켜면 그 브랜치에 앱 사용자를 위한 전용 로그인 서비스가 생깁니다. 이메일과 비밀번호, GitHub와 Google, 세션, 역할과 권한을 갖춘 조직, 교체할 수 있는 서명 키가 포함됩니다. 사용자는 브랜치 자체 데이터베이스 안의 relyt_auth 스키마에 저장되고, 백엔드는 수명이 짧은 JWT를 JWKS URL로 검증하며, Postgres 행 수준 보안이 한 사용자의 행을 다른 사용자로부터 지켜 줍니다.

활성화하는 데는 약 1분이 걸리며, 오픈 소스 @relyt/auth SDK와 완전한 다중 사용자 예제가 GitHub에 있습니다. 바로 시작하고 싶다면 단계별 가이드 Relyt One 관리형 인증 시작하기를 보세요.

이 글에서는 왜 이런 방식으로 만들었는지를 이야기합니다.

인증은 모든 앱에 가장 먼저 필요하고, 가장 먼저 어긋납니다

앱이 개인적인 정보를 저장하는 순간 세 가지가 필요해집니다. 사용자가 로그인할 방법, 백엔드가 누가 호출하는지 알 방법, 그리고 한 사용자가 다른 사용자의 데이터를 절대 볼 수 없다는 보장입니다.

보통은 인증 제공자를 고르고 앱에 연결한 다음, 앱의 수명이 다할 때까지 두 시스템을 일치시키며 지냅니다. 바로 여기서 어려워집니다.

  • 사용자가 다른 곳에 있습니다. 인증 제공자는 자체 데이터베이스에 자체 ID를 가진 자체 사용자 테이블을 갖고 있습니다. 여러분의 데이터에서 사용자를 가리키는 모든 외래 키가 시스템 경계를 넘어갑니다. "주문"을 "사용자"와 조인하려면 API 호출이나 동기화 작업이 필요합니다.
  • 환경이 맞지 않습니다. 데이터베이스에는 프로덕션 사본, 스테이징 사본, 풀 리퀘스트마다 하나씩 브랜치가 있습니다. 반면 인증 제공자에는 보통 프로덕션 테넌트 하나와 많아야 테스트 테넌트 하나가 있을 뿐입니다. 결국 미리 보기 환경은 프로덕션 사용자로 로그인하거나, 어떤 테스트 픽스처와도 맞지 않는 빈 테넌트로 로그인하게 됩니다.
  • 권한 부여가 한곳에서만 적용됩니다. 제공자는 API에 호출자가 누구인지 알려 줍니다. 그 호출자가 특정 행을 볼 수 있는지는 애플리케이션 코드에서 쿼리마다 확인합니다. WHERE user_id = ...를 하나만 빠뜨려도 데이터 유출이 일어납니다.

코딩 에이전트는 이 세 가지를 모두 더 날카롭게 만듭니다. 새 앱을 설정하는 에이전트는 구성된 테넌트가 무엇이든 그것을 상대로 서둘러 인증을 연결하고, 기억나는 곳에서만 권한을 확인합니다.

브랜치에 있는 인증

관리형 인증은 Relyt One의 다른 부분과 같은 생각에서 출발합니다. 데이터에 속한 것은 데이터와 함께 있어야 한다는 것입니다.

브라우저는 브랜치의 인증 URL에서 로그인해 수명이 짧은 JWT를 받고 자체 API를 호출합니다. API는 JWKS로 JWT를 검증하고, relyt_auth가 있는 같은 데이터베이스에서 행 수준 보안을 위해 app.user_id를 설정합니다

브랜치에서 관리형 인증을 활성화하면 다음과 같이 됩니다.

  • 브랜치에 전용 인증 URL이 생깁니다. 사용자는 그곳에서 가입하고 로그인합니다. 이 URL은 앱과는 다른 출처입니다. 로그인 양식은 이 URL로 직접 전송되므로 백엔드는 비밀번호를 받지도, 저장하지도 않습니다.
  • 사용자, 세션, 조직은 여러분의 데이터베이스에 저장됩니다. 해당 브랜치의 relyt_auth 스키마에 있으며, 이들이 소유한 행 바로 옆에서 SQL로 살펴볼 수 있는 행입니다.
  • 브랜치에 JWKS URL이 생깁니다. 이 URL은 브랜치 토큰의 공개 키를 게시합니다. 백엔드에는 공개 키만 있으면 됩니다.
  • 인증 설정은 브랜치에 속합니다. 각 브랜치는 자체 로그인 방법, 출처, 키를 가지며, 브랜치나 프로젝트를 삭제하면 함께 제거됩니다. 인증이 활성화된 브랜치의 하위 브랜치는 사본으로 시작합니다. 신뢰할 수 있는 출처와 이메일 설정을 이어받고, 데이터베이스에는 상위 브랜치 최종 사용자의 사본이 들어 있지만, 세션과 서명 키는 이어받지 않습니다.

이 서비스는 Better Auth를 기반으로 하므로, Better Auth를 써 본 적이 있다면 로그인 엔드포인트와 세션 모델이 익숙할 것입니다.

요청이 인증되는 방식

흐름은 어떤 프레임워크를 쓰든 모든 앱에서 같습니다.

  1. 인증 URL에서 로그인합니다. 브라우저가 이메일과 비밀번호를 브랜치의 인증 URL로 전송하고(또는 GitHub나 Google을 거치고) 세션 토큰을 돌려받습니다.
  2. 세션을 JWT로 교환합니다. SDK가 세션을 EdDSA로 서명된, 15분간 유효한 JSON Web Token으로 교환합니다. SDK는 토큰을 캐시하고 만료되기 전에 갱신합니다.
  3. JWT로 API를 호출합니다. 브라우저는 Authorization: Bearer <jwt>를 보냅니다. 브라우저가 직접 토큰을 검증하지는 않습니다.
  4. 서버에서 검증합니다. API는 브랜치의 JWKS로 서명을 확인하고 발급자, 대상, 만료 시간도 확인한 뒤 sub에서 사용자 ID를 가져옵니다.
  5. Postgres가 적용하도록 합니다. API가 트랜잭션에 사용자 ID를 설정하면, 행 수준 보안 정책이 그 사용자가 볼 수 있는 행을 결정합니다.

코드로 보면 4단계와 5단계는 몇 줄이면 됩니다.

import { getClaims, RelytAuthUnavailableError } from "@relyt/auth/next";

export async function GET(req: Request) {
  let claims;
  try {
    claims = await getClaims(req, { issuer: process.env.RELYT_AUTH_ISSUER! });
  } catch (e) {
    // 키를 가져오지 못함: 503을 반환해 일시적인 네트워크 문제로 모두가 로그아웃되지 않게 함
    if (e instanceof RelytAuthUnavailableError) return new Response(null, { status: 503 });
    throw e;
  }
  if (!claims) return new Response(null, { status: 401 });

  const rows = await db.transaction(async (tx) => {
    await tx.execute(sql`select set_config('app.user_id', ${claims.sub}, true)`);
    return tx.select().from(notes); // 이 사용자의 노트만
  });
  return Response.json(rows);
}
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;

CREATE POLICY own_notes ON notes FOR ALL
  USING (owner_id = current_setting('app.user_id', true))
  WITH CHECK (owner_id = current_setting('app.user_id', true));

이 설정은 트랜잭션 범위이므로 풀링된 연결이 다음 요청으로 설정을 넘기는 일은 없습니다. 코드에서 설정을 잊어버리면 current_setting은 아무것도 반환하지 않고 정책은 어떤 행과도 일치하지 않습니다. 확인이 빠지면 접근이 막힐 뿐, 유출되지는 않습니다.

미리 보기에 포함된 기능

로그인 방법

  • 이메일과 비밀번호.
  • GitHub와 Google. OAuth 앱의 Client ID와 Secret을 콘솔에 붙여 넣으면 됩니다. Secret은 쓰기 전용이며 다시 표시되지 않습니다. 출처를 넘나드는 소셜 로그인은 PKCE와 함께 일회용 전달 코드를 사용하므로 서드 파티 쿠키 없이도 작동합니다.

리디렉션과 출처

인증 URL은 여러분이 등록한 신뢰할 수 있는 출처(예: https://app.example.com, http://localhost:3000, 와일드카드 불가)의 브라우저에만 응답합니다. 소셜 로그인은 정확히 등록된 콜백 URL로만 리디렉션합니다. 두 목록 모두 브랜치마다 따로 있으므로, 미리 보기 브랜치는 프로덕션까지 신뢰하게 만들지 않고도 https://pr-128.example.dev를 신뢰할 수 있습니다.

토큰과 키

  • JWT는 15분 동안 유효하며 EdDSA로 서명됩니다. SDK는 EdDSA만 허용하므로 다른 알고리즘으로 서명된 토큰은 거부됩니다.
  • 서명 키는 필요할 때 교체할 수 있습니다. 교체하면 다음 토큰부터 새 키로 서명하고, 이전 키는 960초(약 16분) 동안 JWKS에 남아 있어 이미 발급된 토큰이 계속 검증되며, 에지 노드는 약 30초 안에 새 키 세트를 반영합니다. 이 유예 기간 동안 이전 키는 최대 3개까지만 보관되므로, 그 안에 추가로 교체하려 하면 이전 키 하나가 만료될 때까지 거부됩니다. 검증 측은 알 수 없는 키 ID를 만나면 JWKS를 다시 가져와야 하며, @relyt/auth는 그렇게 합니다.
  • 세션은 폐기할 수 있습니다. 한 사용자의 세션만, 또는 브랜치의 모든 사용자 세션을 폐기할 수 있습니다. 폐기된 세션은 더 이상 갱신하거나 새 토큰으로 교환할 수 없습니다. 이미 발급된 JWT는 만료될 때까지(최대 15분) 유효하므로, 접근을 즉시 막아야 한다면 앱에서도 확인하세요.

조직, 역할, 권한

사용자는 조직을 만들 수 있고, 소유자와 관리자는 인증 URL의 조직 엔드포인트로 멤버를 초대할 수 있습니다(미리 보기에서는 초대 이메일이나 인증 이메일을 보내지 않으므로, 초대받은 사람은 인증된 이메일로 로그인해야 하며 현재로서는 GitHub나 Google을 뜻합니다). 각 멤버에게는 역할이 있으며, 콘솔에서 자체 리소스와 작업을 정의해 역할에 부여할 수 있습니다. 예를 들어 create, read, update, delete 작업을 가진 todo 리소스를 정의할 수 있습니다. 기본 제공 역할은 owner, admin, member이며, 사용자 지정 역할은 최대 16개, 사용자 지정 리소스는 최대 32개까지 추가할 수 있습니다.

사용자에게 활성 조직이 있으면 JWT에 그 정보가 담깁니다.

"o": {
  "id": "…",
  "slug": "acme",
  "role": "member",
  "perms": ["todo:create", "todo:read", "todo:update"]
}

API는 작업에 대해 o.perms를 확인하고, org_id에 대한 RLS 정책이 어떤 팀 행을 볼 수 있는지 결정합니다. 권한 목록은 토큰에 들어가야 하므로 콘솔은 resource:action 쌍을 합계 64개, 1 KiB로 제한합니다. 만약 들어가지 않으면 perms는 null이 되며, API는 추측하지 말고 거부해야 합니다.

운영

  • Users(사용자)에는 브랜치의 최종 사용자와 이메일 인증 여부, 역할, 활성 세션, 마지막 활동이 표시되며, 로드된 목록 안에서 검색할 수 있습니다.
  • Integration code(연동 코드)에는 환경 변수와 함께 Next.js 미들웨어, 라우트 핸들러, 브라우저 클라이언트, 소셜 로그인용 코드 조각이 브랜치의 URL로 채워져 표시됩니다.
  • Disable auth(인증 비활성화)는 사용자, 설정, 키를 삭제하지 않고 로그인만 중단합니다. 다시 활성화하면 모든 것이 그대로 남아 있습니다.

주의할 점: BYPASSRLS

행 수준 보안은 앱이 정책이 적용되는 역할로 연결할 때만 보호 효과가 있습니다. Relyt One 콘솔에서 만든 데이터베이스 사용자에게는 BYPASSRLS가 있으며, Postgres는 이런 사용자에 대해 모든 정책을 조용히 건너뜁니다. RLS에 의존하는 앱이라면 SQL로 전용 애플리케이션 역할을 만드세요.

CREATE ROLE app_user LOGIN NOBYPASSRLS NOSUPERUSER NOCREATEROLE NOCREATEDB;
\password app_user
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON notes TO app_user;

\password는 비밀번호를 입력받는 psql 명령이므로 비밀번호가 셸 기록에 남지 않습니다. 그런 다음 SELECT rolbypassrls FROM pg_roles WHERE rolname = 'app_user';로 역할을 확인하세요. 결과는 f여야 합니다. 예제 앱의 테스트 모음에는 이 설정을 잘못했을 때 어떤 일이 생기는지 보여 주는 테스트 케이스가 있습니다.

미리 보기 제한 사항

관리형 인증은 미리 보기 단계이며, 사람들이 어떻게 사용하는지 알아 가는 동안 일부 기능은 의도적으로 작게 유지하거나 제외했습니다.

  • 조직 소유자와 관리자만 인증을 활성화하고 설정을 변경할 수 있습니다.
  • 하위 브랜치는 소셜 로그인 자격 증명을 복사하지 않으며, GitHub OAuth 앱에는 콜백 URL을 하나만 등록할 수 있으므로 브랜치마다 OAuth 앱을 하나씩 사용하세요.
  • 아직 이메일을 보내지 않습니다. 인증, 비밀번호 재설정, 초대 이메일이 없습니다.
  • 정식 출시 전에 이름, 한도, 콘솔 레이아웃이 바뀔 수 있습니다.

사용해 보기

  1. 콘솔에서 브랜치를 열고 Auth(인증)로 이동한 뒤 Enable auth(인증 활성화)를 선택하세요.
  2. Relyt One 관리형 인증 시작하기를 따라 앱에 로그인, 토큰 검증, RLS를 추가하세요.
  3. 또는 TODO 예제를 클론하세요. 개인 TODO와 팀 TODO, 테스트 모음으로 이 모든 것을 보여 줍니다.

무엇이 깨지는지 알려 주세요. 지금이 바로 무엇이든 바꿀 수 있는 미리 보기 기간입니다.

관리형 인증 사용해 보기

태그인증Postgres보안

작성자

Zhan Lu

다음 데이터베이스는 브랜치 하나면 됩니다.

1분 안에 무료 프로젝트를 만드세요. 신용카드도, 설정도 필요 없습니다.

블로그로 돌아가기