Nodaro 문서
문서노드 레퍼런스모델템플릿AI 에이전트(MCP)개발자셀프 호스팅리서치비교

외부 로그인(SSO)

신뢰할 수 있는 ID 공급자가 서명된 JWT 어서션이나 OIDC, SAML 옵션으로 사용자를 Nodaro 설치에 로그인시키게 하고, 계정 연결 방식도 제어하세요.

외부 로그인(SSO)을 사용하면 신뢰할 수 있는 ID 공급자(IdP)가 사용자를 Nodaro 설치에 로그인시킬 수 있습니다. 주요 연동 방식에서는 IdP가 수명이 짧은 JWT 어서션에 서명하고, Nodaro가 이를 검증하며, 브라우저는 일반 Nodaro 세션을 받습니다. 설치 운영자가 공급자를 하나 이상 구성하기 전까지 SSO는 꺼져 있습니다.

사용자가 최종적으로 들어가는 계정은 일반 계정입니다. SSO는 특별한 자격 증명을 추가하지 않습니다. 세션을 시작하는 방법일 뿐입니다.

두 가지 연동 방식

종류대상작동 방식
assertionOIDC나 SAML을 지원하지 않는 발급자. 예를 들어 수명이 짧은 서명 토큰만 발급할 수 있는 임베딩 호스트IdP가 JWT에 서명하면, Nodaro가 이를 검증하고 세션으로 교환합니다. 이 방식은 처음부터 끝까지 작동합니다.
oidc, saml표준 OpenID Connect나 SAML을 지원하는 IdP로그인 페이지가 설치 자체의 Supabase Auth로 표준 로그인을 시작하고, Supabase Auth가 IdP의 응답을 검증합니다. 이 방식에는 제한이 있습니다. OIDC와 SAML 공급자를 참조하세요.

공급자 구성

서버의 EXTERNAL_SSO_PROVIDERS를 공급자의 JSON 배열로 설정하세요. 값을 직접 쓰거나 @/path/to/providers.json 형식으로 지정합니다(맨 앞의 @는 그 경로의 파일을 읽으라는 뜻입니다). 값의 형식이 잘못되면 서버가 시작할 때 멈추므로, 오타 때문에 공급자가 조용히 빠지거나 로그인이 절반만 구성되는 일은 없습니다.

[
  {
    "id": "acme-chat",
    "label": "Acme Chat",
    "kind": "assertion",
    "secret": "a-dedicated-32-character-hmac-key-for-nodaro-only",
    "audience": "nodaro",
    "claimMap": { "email": "email", "emailVerified": "email_verified", "subject": "sub" },
    "initiateUrl": "https://chat.example.com/oauth/nodaro",
    "maxLifetimeSeconds": 300
  },
  {
    "id": "keycloak",
    "label": "Acme (Keycloak)",
    "kind": "oidc",
    "supabaseProvider": "keycloak"
  }
]
필드적용 대상의미
id모두공급자의 슬러그로, 경로 /v1/sso/:id에 사용되며 연결된 각 계정에 저장됩니다. 1~63자이며, 첫 글자는 영문 소문자나 숫자이고, 그 뒤로는 영문 소문자, 숫자, _, -를 쓸 수 있습니다. 점은 쓸 수 없습니다. 고유해야 합니다.
label모두공급자의 표시 이름입니다.
kind모두assertion, oidc, saml 중 하나입니다.
secretassertion, 필수어서션의 서명을 검증하는 HS256 키로, 16자 이상입니다. 전용 시크릿을 사용하고, IdP 자체의 세션 서명 시크릿은 절대 사용하지 마세요. 그래야 Nodaro가 침해되더라도 IdP 세션을 위조할 수 없습니다.
audienceassertion, 필수어서션의 aud 클레임과 같아야 하는 값입니다.
claimMapassertion, 선택이메일, 인증 여부 플래그, 서브젝트를 각각 어떤 클레임이 담는지 지정합니다. 기본값은 email, email_verified, sub입니다.
initiateUrlassertion, 선택SSO 버튼이 사용자를 보내는 주소입니다. 이 값이 없으면 버튼을 클릭했을 때 400 no_assertion으로 응답합니다.
initiateUrlByHostassertion, 선택포트가 없는 소문자 호스트 이름을, 그 호스트에서 initiateUrl 대신 쓸 주소에 대응시키는 맵입니다. 여러 호스트 이름으로 응답하는 배포 환경을 위한 것입니다.
maxLifetimeSecondsassertion, 선택Nodaro가 허용하는 최대 수명(exp에서 iat를 뺀 값)입니다. 기본값은 300초이고, 최댓값은 3,600초입니다.
domainoidc, saml시작할 때 검증되지만 사용되지는 않습니다. OIDC와 SAML 공급자를 참조하세요.
supabaseProvideroidc시작할 때 검증되지만 사용되지는 않습니다.
  • initiateUrl은 IdP에 아무것도 전달하지 않습니다. 로그인 후 사용자가 특정 페이지에 도착하게 하려면, IdP가 다시 리디렉션할 때 &next=<path>를 추가하게 하세요.
  • initiateUrlByHost의 키는 시작할 때 검증됩니다. 대문자나 포트가 들어 있는 키가 있으면 서버가 멈춥니다. 이 맵은 공개되지 않으며, 계정 연결은 사용자가 어느 호스트로 들어왔는지와 관계가 없습니다.

어서션 규약

IdP가 JWT를 발급하고 브라우저를 GET /v1/sso/:provider?assertion=<jwt>로 보냅니다. Nodaro는 다음 규칙을 모두 충족할 때만 어서션을 허용합니다.

  • 알고리즘. JWT는 HS256과 공급자의 secret으로 서명됩니다.
  • 대상. aud가 공급자의 audience와 같습니다.
  • 만료. exp가 있고 과거 시각이 아닙니다. 시계 차이는 5초까지 허용됩니다.
  • 수명. exp에서 iat를 뺀 값이 maxLifetimeSeconds 이하입니다. 이 값은 Nodaro의 시계로 측정합니다. 5초 넘게 미래를 가리키는 iat는 보정되고, iat가 없으면 현재 시각으로 간주합니다.
  • 일회용. jti가 있고 고유합니다. Nodaro는 어서션 자체의 유효 기간이 지날 때까지 각 jti를 기억하고, 두 번째에는 거부합니다.
  • 이메일. 이메일 클레임이 있습니다.
  • 인증된 이메일. 계정을 만들거나 연결하려면 인증 여부 플래그가 true여야 합니다. 플래그가 없으면 인증되지 않은 것으로 간주합니다.

어서션이 규칙을 어기면 401과 함께 invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayed 중 하나로 응답합니다. 이 엔드포인트는 IP 주소당 60초에 20개의 요청을 받습니다. 이를 넘으면 Retry-After 헤더와 함께 429 rate_limit_exceeded로 응답합니다.

다음은 IdP 쪽 예제로, Node.js용 jose 라이브러리로 유효한 어서션을 발급합니다.

import { SignJWT } from "jose"
import { randomUUID } from "node:crypto"

const secret = new TextEncoder().encode(process.env.NODARO_SSO_SECRET)

const assertion = await new SignJWT({ email: user.email, email_verified: true })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject(user.id)
  .setAudience("nodaro")
  .setIssuedAt()
  .setExpirationTime("2m")
  .setJti(randomUUID())
  .sign(secret)

const url = new URL("https://nodaro.example.com/v1/sso/acme-chat")
url.searchParams.set("assertion", assertion)
url.searchParams.set("next", "/projects")
res.redirect(url.toString())

로그인 흐름

  1. 사용자가 Nodaro 로그인 페이지에서 SSO 버튼을 클릭하면, 버튼이 어서션 없이 GET /v1/sso/:provider를 호출합니다.
  2. Nodaro가 공급자의 initiateUrl로 리디렉션(302)합니다.
  3. IdP가 사용자를 인증하고 브라우저를 GET /v1/sso/:provider?assertion=<jwt>로 돌려보냅니다. 선택적으로 &next=<path>를 붙입니다.
  4. Nodaro가 어서션을 검증하고, 재사용을 거부하고, 계정 연결 규칙을 적용합니다.
  5. Nodaro가 /sso?sso_token=<one-time token>으로 리디렉션합니다. 이 페이지가 일회용 토큰을 세션으로 교환합니다.
  6. 사용자는 /projects 또는 next 경로에 도착합니다.

임베딩 호스트 같은 IdP는 3단계부터 시작할 수도 있습니다. 새 어서션을 붙여 브라우저를 교환 엔드포인트로 리디렉션하면 됩니다. 로그인 페이지 자체의 ?redirect= 파라미터는 SSO 리디렉션을 거치며 전달되지 않습니다. 전달되는 것은 IdP가 추가한 next뿐입니다.

로그인 페이지에 SSO 버튼 표시

로그인 페이지는 다음 두 조건을 모두 충족할 때 SSO 버튼을 표시합니다.

  • 배포 환경의 서피스 프로필이 auth.methods에 sso를 포함하고 auth.ssoLabel을 설정합니다. ssoLabel이 없는 프로필에서는 메서드에서 sso가 빠집니다. 버튼 텍스트는 ssoLabel입니다.
  • 공급자가 하나 이상 구성되어 있습니다. 페이지는 GET /v1/sso/providers로 이를 확인합니다.

서피스 프로필은 Business 에디션과 Cloud 에디션의 기능입니다. 에디션과 서피스 프로필을 참조하세요.

auth.methods에 sso만 나열한 프로필은 서버 측 제한도 켭니다. 로그인한 모든 계정은 SSO를 통해 만들어지거나 연결된 계정이어야 하며, 그 밖의 세션은 첫 API 호출에서 403 sso_required로 거부됩니다. 유일한 예외는 아래에서 설명하는 배포 환경의 결제 계정입니다. 메서드에 email을 추가하면 설치 전체에서 이 제한이 꺼집니다.

계정 연결 방식

어서션이 유효하면 Nodaro는 규칙에 따라 계정을 찾거나 만듭니다. 이 규칙은 이메일 주소가 같을 뿐인 기존 계정을 어서션이 절대 가로챌 수 없도록 설계되었습니다.

상황결과
그 이메일을 가진 계정이 없고, 이메일이 인증됨새 계정이 만들어지고 공급자에 연결됩니다.
그 이메일을 가진 계정이 없고, 이메일이 인증되지 않음403 email_unverified로 거부됩니다. 따라서 인증되지 않은 클레임으로 실제 주소를 선점할 수 없습니다.
계정이 이미 이 공급자에 연결됨사용자가 로그인됩니다.
계정이 다른 공급자에 연결됨EXTERNAL_SSO_LINK_EXISTING이 켜져 있어도 403 account_linked_other_provider로 거부됩니다.
SSO 없는 로컬 계정이 있고, EXTERNAL_SSO_LINK_EXISTING이 켜져 있으며, 이메일이 인증됨계정이 공급자에 연결되고 사용자가 로그인됩니다.
SSO 없는 로컬 계정이 있고, 플래그가 꺼져 있거나 이메일이 인증되지 않음403 account_exists로 거부됩니다.

EXTERNAL_SSO_LINK_EXISTING은 기본적으로 꺼져 있으며, 이것이 계정 탈취에 안전한 설정입니다. 대소문자와 관계없이 true 또는 1만 이 설정을 켭니다. IdP의 인증된 이메일 클레임을 이미 있는 계정에 연결해도 될 만큼 신뢰할 때만 켜세요.

account_exists와 email_unverified 거부는 모든 계정에 같은 문구를 사용하므로, 로그인 양식으로 어떤 주소가 특별한 역할을 가졌는지 알아낼 수 없습니다. account_exists는 주소가 둘 이상의 계정과 일치할 때, 조회가 실패할 때, 두 로그인이 같은 계정을 동시에 만들려고 경쟁할 때도 반환됩니다.

결제 계정이 있는 배포 환경

배포 환경은 설치의 모든 사용자 비용을 내는 결제 계정 하나를 지정할 수 있습니다. 외부 지갑을 참조하세요. 이 계정은 배포 환경의 크레딧을 보유하므로 규칙이 더 엄격합니다.

  • 처음으로 인증된 로그인을 할 때 연결됩니다. EXTERNAL_SSO_LINK_EXISTING 설정과는 관계없습니다. 이 계정에 대한 인증되지 않은 어서션은 email_unverified로, 다른 공급자의 어서션은 account_linked_other_provider로 거부됩니다.
  • 이후의 모든 로그인을 다시 확인합니다. 이메일이 인증되어 있어야 하고, 서브젝트가 처음 계정을 연결한 서브젝트와 같아야 합니다. 그렇지 않으면 로그인이 403 account_linked_other_subject로 거부됩니다.
  • 비상용 수단으로 비밀번호를 유지합니다. IdP가 이 계정을 어서트할 수 없게 되는 날을 위한 것입니다. SSO 전용 배포 환경에서는 그 양식이 /login?billing=1에 있으며, 결제 계정만 들어갈 수 있습니다.
  • 설치 관리자가 차단하거나 삭제할 수 없습니다(403 payer_account_protected). 따라서 배포 환경 자체의 IdP가 나눠 준 관리자 역할로 설치를 크레딧에서 차단할 수 없습니다.

IdP에서 결제 계정의 서브젝트가 바뀌면, 그 계정의 SSO 로그인은 계속 account_linked_other_subject로 실패합니다. 이 계정은 여전히 비밀번호로 로그인할 수 있습니다.

플랫폼 운영자의 주소(PLATFORM_OPERATOR_EMAILS, 이 값이 비어 있으면 PLATFORM_OWNER_EMAIL)는 결제 계정이 있는 배포 환경에서 새로 연결되지 않습니다. 이 주소들은 account_exists로 거부되므로, 운영자 계정은 SSO 밖에 머뭅니다. 이미 연결된 운영자 계정은 계속 로그인할 수 있습니다.

OIDC와 SAML 공급자

oidc와 saml 종류는 로그인을 설치 자체의 Supabase Auth에 넘기고, Supabase Auth가 IdP의 응답을 검증합니다. 둘 다 제한이 있습니다.

  • oidc: 로그인 페이지는 공급자의 id를 OAuth 공급자 이름으로 전달합니다. 따라서 id 자체가 설치의 Supabase Auth에 구성된 OAuth 공급자의 이름(예: keycloak, azure)이어야 합니다.
  • saml: 로그인 페이지는 공급자의 id를 SAML 도메인으로 전달합니다. id에는 점을 넣을 수 없으므로 acme.com 같은 도메인을 표현할 수 없고, saml은 처음부터 끝까지 작동하지 않습니다.
  • oidc 또는 saml 공급자로 보낸 어서션은 400 not_assertion_provider로 거부됩니다.

엔드포인트

두 엔드포인트 모두 공개되어 있으며 토큰이 필요 없습니다. /v1/sso/ 아래의 경로는 이 두 개뿐입니다.

메서드경로하는 일
GET/v1/sso/providers로그인 페이지에 쓸 공급자 목록을 반환합니다. id, label, kind만 포함하며, 시크릿은 절대 포함하지 않습니다. SSO가 꺼져 있으면 빈 목록을 반환합니다.
GET/v1/sso/:provider교환 엔드포인트입니다. JSON API가 아니라 브라우저 리디렉션 엔드포인트입니다.
상태코드발생 시점
400no_assertion버튼을 클릭했는데, 공급자에 initiateUrl이 없습니다.
400not_assertion_provideroidc 또는 saml 공급자로 어서션을 보냈습니다.
401invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayed어서션이 규약의 규칙을 어겼습니다.
403email_unverified, account_exists, account_linked_other_provider, account_linked_other_subject계정 연결 규칙이 로그인을 거부했습니다.
403sso_requiredSSO 전용 배포 환경이 SSO를 거치지 않은 세션을 거부했습니다.
404unknown_provider그 id를 가진 공급자가 없거나, SSO가 꺼져 있습니다.
429rate_limit_exceeded한 IP 주소에서 요청이 너무 많습니다.

보안 참고 사항

  • 전용 시크릿을 사용하세요. secret은 Nodaro 전용 검증 키이며, IdP 자체의 세션 시크릿과 분리되어 있습니다.
  • 어서션은 한 번만 작동합니다. jti 검사가 제출된 어서션을 두 번째에는 거부하고, 수명 상한이 유효 시간을 짧게 유지합니다.
  • 리디렉션은 설치 안에 머뭅니다. next는 같은 설치의 상대 경로이고, /로 시작하되 //로 시작하지 않을 때만 적용됩니다. 그 밖의 경우에는 /projects에 도착합니다.
  • 시크릿은 서버 밖으로 나가지 않습니다. GET /v1/sso/providers는 id, label, kind만 반환합니다. 어서션과 일회용 토큰은 요청 로그에서 제거됩니다.
  • 교환에는 요청 한도가 있습니다. IP 주소별로 적용됩니다.

자주 묻는 질문

Nodaro에서 SSO는 기본적으로 켜져 있나요?

아니요. EXTERNAL_SSO_PROVIDERS에 공급자를 하나 이상 지정하기 전까지는 로그인 페이지에 SSO 버튼이 표시되지 않고, GET /v1/sso/providers는 빈 목록을 반환하며, 모든 공급자 경로는 404로 응답합니다.

SSO 어서션은 어떻게 서명해야 하나요?

HS256과, 해당 공급자용으로 구성하고 다른 용도로는 쓰지 않는 시크릿으로 서명합니다. 어서션에는 aud, exp, 고유한 jti, 이메일도 들어 있어야 하며, Nodaro는 각 어서션을 한 번만 허용합니다.

SSO 어서션으로 이메일이 같은 기존 계정을 가로챌 수 있나요?

기본적으로는 불가능합니다. 기존 로컬 계정은 EXTERNAL_SSO_LINK_EXISTING이 true이고, 어서션이 이메일이 인증되었다고 밝힐 때만 연결됩니다. 이미 다른 공급자에 연결된 계정은 다시 연결되지 않습니다.

SSO 사용자는 특별한 종류의 Nodaro 계정인가요?

아니요. 교환이 끝나면 사용자는 일반 세션을 갖습니다. SSO는 그 세션을 시작하는 방법일 뿐입니다.

Nodaro는 OIDC와 SAML ID 공급자를 지원하나요?

일부 지원합니다. oidc와 saml 종류는 로그인을 설치 자체의 인증 서비스에 넘기며, 이 페이지에서 설명하는 제한이 있습니다. 처음부터 끝까지 작동하는 방식은 어서션 교환입니다.

최종 업데이트

목차