# 싱글 사인온

> 신뢰할 수 있는 ID 공급자가 셀프 호스팅 Nodaro에 사용자를 로그인시키게 하세요. EXTERNAL_SSO_PROVIDERS 구성, 계정 연결 규칙, SSO 전용 로그인을 다룹니다.

Source: https://nodaro.ai/ko/docs/self-hosting/sso

**싱글 사인온**(SSO)을 사용하면 신뢰할 수 있는 외부 ID 공급자가 셀프 호스팅 Nodaro에 사용자를 로그인시킬 수 있습니다. 각 공급자를 `EXTERNAL_SSO_PROVIDERS` 변수에 기술하면 로그인 페이지에 SSO 버튼이 표시됩니다. 사용자가 최종적으로 들어가는 계정은 내 설치의 일반 사용자입니다. SSO는 세션을 시작하는 방법일 뿐입니다. 이 페이지는 운영자 쪽 설정을 다루며, 프로토콜은 [개발자용 외부 로그인(SSO)](https://nodaro.ai/docs/developers/sso)에 있습니다.

## 사용할 공급자 종류
| 종류 | 사용 시기 | 상태 |
| --- | --- | --- |
| `assertion` | 발급자가 OpenID Connect나 SAML을 지원하지 않지만 수명이 짧은 JWT에 서명할 수 있을 때입니다. 예를 들어 Nodaro를 임베딩하는 앱입니다. Nodaro가 토큰을 검증하고 일반 세션으로 교환합니다. | 처음부터 끝까지 작동합니다. |
| `oidc` | ID 공급자가 OpenID Connect를 지원하고 내 설치의 Supabase Auth에 구성되어 있을 때입니다. 브라우저가 일반적인 Supabase 로그인 리디렉션을 거치고, Supabase가 결과를 검증합니다. | 공급자의 `id`는 Supabase Auth에 있는 OAuth 공급자의 이름이어야 합니다. 예: `keycloak`, `azure`. |
| `saml` | — | 항목은 검증을 통과하지만, 로그인 페이지가 SAML 도메인을 보낼 수 없으므로 아무도 로그인시키지 않습니다. `assertion`이나 `oidc`를 쓰세요. |

SSO는 기본적으로 꺼져 있습니다. `EXTERNAL_SSO_PROVIDERS`가 설정되지 않으면 로그인 페이지에 SSO 버튼이 표시되지 않고, `GET /v1/sso/providers`는 빈 목록을 반환하며, 모든 공급자 경로가 `404 unknown_provider`로 응답합니다.

## 공급자 구성
`EXTERNAL_SSO_PROVIDERS`를 JSON 배열로 설정하세요. 값을 직접 쓰거나, 파일을 읽으려면 `@/path/to/providers.json` 형식으로 지정합니다.

```json
[
{
"id": "librechat",
"label": "LibreChat",
"kind": "assertion",
"secret": "a-dedicated-32+char-hmac-key-not-the-idp-session-secret",
"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"
}
]
```

형식이 잘못된 값은 **부팅을 멈춥니다**. 시크릿이 들어 있는 항목의 오타 때문에 공급자가 조용히 빠져서는 안 됩니다. 그러면 SSO가 `404`로 실패하는 것처럼 보이거나 로그인이 절반만 구성되기 때문입니다.

Compose 스택에서는 Compose 파일이 `.env`에서 `EXTERNAL_SSO_PROVIDERS`와 `EXTERNAL_SSO_LINK_EXISTING`을 전달하지 않으므로, `nodaro` 서비스의 `environment:` 아래에 추가하세요. 파일을 쓰려면 컨테이너에 파일을 마운트하고 컨테이너 안의 경로를 지정하세요.

### 공급자 필드
| 필드 | 적용 대상 | 역할 |
| --- | --- | --- |
| `id` | 전체 | URL에 안전한 이름입니다. 첫 글자는 소문자나 숫자이고, 그 뒤로 소문자, 숫자, `_`, `-`를 쓸 수 있으며, 전체 1~63자이고 점은 쓸 수 없습니다. `/v1/sso/<id>` 경로에 나타나며 사용자에게 저장됩니다. 고유해야 합니다. |
| `label` | 전체 | 공급자의 이름입니다. 공급자가 두 개 이상이고 서피스 프로필에 `auth.ssoLabel`이 없을 때만 로그인 버튼에 나타납니다. |
| `kind` | 전체 | `assertion`, `oidc`, `saml` 중 하나입니다. |
| `secret` | `assertion`, 필수 | 어서션의 서명을 검증하는 HS256 키이며, 16자 이상이어야 합니다. ID 공급자 자체의 세션 시크릿이 아닌 전용 키를 쓰세요. 그래야 Nodaro 쪽에서 유출되어도 공급자의 세션을 위조할 수 없습니다. |
| `audience` | `assertion`, 필수 | 어서션의 `aud` 클레임과 같아야 하는 값입니다. |
| `claimMap` | `assertion` | 이메일, 인증 여부 플래그, 주체를 담은 클레임입니다. 기본값은 `{ "email": "email", "emailVerified": "email_verified", "subject": "sub" }`입니다. |
| `initiateUrl` | `assertion` | 절대 URL입니다. 사용자가 로그인 버튼을 클릭하면 Nodaro가 이곳으로 리디렉션합니다. 공급자가 사용자를 로그인시킨 뒤 어서션과 함께 돌아옵니다. 이 값이 없으면 클릭했을 때 `400 no_assertion`으로 응답합니다. |
| `initiateUrlByHost` | `assertion` | 포트 없는 소문자 호스트 이름을 절대 URL에 대응시키는 맵입니다. 해당 호스트로 들어온 클릭은 `initiateUrl` 대신 그 URL로 갑니다. 여러 호스트 이름으로 응답하는 설치용입니다. 잘못된 키는 부팅을 멈추며, 이 맵은 절대 공개되지 않습니다. |
| `maxLifetimeSeconds` | `assertion` | 어서션이 가질 수 있는 최대 수명(`exp` 빼기 `iat`)입니다. 기본값은 `300`(5분)이고 최댓값은 `3600`입니다. |
| `supabaseProvider` | `oidc` | 검증은 하지만 쓰이지 않습니다. 로그인 페이지는 공급자의 `id`를 Supabase에 전달합니다. |
| `domain` | `oidc`, `saml` | 검증은 하지만 쓰이지 않습니다. `oidc`나 `saml` 항목에는 `domain` 또는 `supabaseProvider`가 필요합니다. |

공개되는 것은 `id`, `label`, `kind`뿐이며, `GET /v1/sso/providers`로만 공개됩니다. 시크릿은 서버를 벗어나지 않습니다.

## Nodaro가 어서션에서 확인하는 것
`assertion` 공급자의 ID 서버는 JWT에 서명하고 브라우저를 `GET /v1/sso/<id>?assertion=<jwt>`로 보냅니다. Nodaro는 다음 조건을 모두 충족할 때만 이를 받습니다.

- **HS256**과 공급자의 `secret`으로 서명되어 있어야 합니다.
- **`aud`** 값이 공급자의 `audience`와 같아야 합니다.
- **`exp`** 값이 있고 5초의 허용 오차 안에서 지나지 않았어야 하며, 수명이 `maxLifetimeSeconds` 이내여야 합니다.
- **`jti`** 값이 있고 한 번도 쓰인 적이 없어야 합니다. 각 어서션은 한 번만 작동합니다.
- **이메일** 클레임이 있어야 합니다. 계정을 만들거나 연결하려면 **인증 여부** 클레임이 `true`여야 합니다.

확인에 실패하면 `401`로 응답합니다. 이 경로는 IP 주소 하나당 60초에 20회까지 요청을 허용하고, 그다음에는 `429`로 응답합니다. 전체 계약, 오류 코드, 리디렉션 흐름은 [개발자용 외부 로그인(SSO)](https://nodaro.ai/docs/developers/sso)에 있습니다.

## 계정 연결 방식
어서션이 통과하면 Nodaro는 일치하는 사용자를 찾거나 만듭니다. 이 규칙은 이메일 주소가 같다는 이유만으로 어서션이 기존 계정을 가로채는 일이 절대 없도록 합니다.

| 상황 | 결과 |
| --- | --- |
| 해당 이메일의 계정이 없고, 이메일이 인증됨 | 새 사용자가 만들어지고 공급자에 연결됩니다. |
| 해당 이메일의 계정이 없고, 이메일이 인증되지 않음 | 거부: `403 email_unverified`. |
| 계정이 이미 이 공급자에 연결됨 | 로그인됩니다. |
| 계정이 다른 공급자에 연결됨 | 거부: `403 account_linked_other_provider`. `EXTERNAL_SSO_LINK_EXISTING=true`여도 마찬가지입니다. |
| SSO 없는 로컬 계정이 있고, `EXTERNAL_SSO_LINK_EXISTING=true`이며, 이메일이 인증됨 | 계정이 공급자에 연결되고 로그인됩니다. |
| SSO 없는 로컬 계정이 있고, 플래그가 `false`이거나 이메일이 인증되지 않음 | 거부: `403 account_exists`. |

`EXTERNAL_SSO_LINK_EXISTING`의 기본값은 `false`이며, 계정 탈취에 안전한 설정입니다. 대소문자와 관계없이 `true` 또는 `1`만 이 기능을 활성화합니다. 공급자의 인증된 이메일 클레임을 기존 계정에 붙여도 될 만큼 신뢰할 때만 활성화하세요.

`account_exists`와 `email_unverified`는 모든 계정에 같은 문구를 쓰므로, 로그인 양식으로 어떤 주소가 있는지 알아낼 수 없습니다. 서버 로그에는 어떤 계정이 대상이었는지 기록됩니다.

## 로그인 버튼
공급자가 하나 이상 구성되면 로그인 페이지에 SSO 버튼이 표시됩니다.

- **공급자가 하나일 때:** 버튼에 **싱글 사인온** 또는 서피스 프로필의 `auth.ssoLabel`이 표시됩니다.
- **공급자가 여러 개일 때:** 서피스 프로필에 `auth.ssoLabel`이 없으면 각 버튼에 해당 공급자의 `label`이 표시됩니다.

로그인한 뒤 사용자는 `/projects`로 이동합니다. 공급자는 다시 리디렉션할 때 `&next=<path>`를 추가해 다른 페이지로 보낼 수 있습니다. Nodaro는 이 값이 같은 출처의 상대 경로이고, `/`로 시작하되 `//`로 시작하지 않을 때만 따릅니다. 그 밖의 값은 `/projects`로 갑니다. `CORS_ORIGIN`에 나열된 호스트에서는 도착 리디렉션이 상대 경로로 유지되어, 사용자가 들어온 호스트에 머뭅니다.

## SSO로만 로그인 허용
Business 에디션에서는 서피스 프로필로 다른 로그인 방법을 없앨 수 있습니다.

```bash
NODARO_SURFACE_PROFILE={"auth":{"methods":["sso"],"ssoLabel":"Sign in with Acme"}}
```

`sso`만 나열한 목록은 서버 쪽 규칙도 활성화합니다. 로그인한 모든 계정이 SSO를 통해 만들어졌거나 연결되어 있어야 합니다. 다른 세션은 첫 API 호출에서 `403 sso_required`로 거부되므로, 로그인 서비스에 직접 등록한 계정은 설치를 쓸 수 없습니다. 목록에 `email`을 추가하면 이 규칙이 꺼집니다. [에디션과 서피스 프로필](https://nodaro.ai/docs/self-hosting/editions-and-profiles#sign-in-methods)을 참조하세요.

## 보안 요약
- 공급자마다 공급자 자체의 세션 시크릿과 다른 **전용 시크릿**.
- 서버가 강제하는 짧은 수명의 **일회용 어서션**.
- 로그인 후에는 **같은 출처로만 리디렉션**.
- **시크릿은 서버를 벗어나지 않습니다.** 어서션과 일회용 토큰은 요청 로그에서 제거됩니다.
- IP 주소별로 교환 경로에 **요청 한도** 적용.
- **일반 세션.** 교환 후 사용자는 특별한 자격 증명이 없는 일반 사용자입니다.

## 자주 묻는 질문

### 싱글 사인온은 기본적으로 켜져 있나요?

아니요. EXTERNAL_SSO_PROVIDERS가 설정되지 않으면 로그인 페이지에 SSO 버튼이 표시되지 않고, 모든 SSO 경로가 404로 응답합니다. 활성화하려면 공급자를 하나 이상 구성하세요.

### 어떤 종류의 공급자를 구성해야 하나요?

Nodaro를 임베딩하는 앱처럼 수명이 짧은 서명 토큰을 발급할 수 있는 발급자에는 assertion을 쓰세요. 내 설치의 Supabase Auth에 구성한 ID 공급자에는 oidc를 쓰세요. saml 종류의 항목은 검증은 통과하지만 아무도 로그인시키지 않습니다.

### SSO가 이메일이 같은 기존 계정을 가로챌 수 있나요?

기본적으로는 불가능합니다. EXTERNAL_SSO_LINK_EXISTING이 false이므로, 이미 로컬 계정이 있는 이메일의 어서션은 account_exists로 거부됩니다. 공급자의 인증된 이메일 클레임을 신뢰할 때만 true로 설정하세요.

### SSO로만 로그인하도록 하려면 어떻게 하나요?

Business 에디션에서 서피스 프로필의 auth.methods를 ["sso"]로 설정하고 auth.ssoLabel을 지정하세요. 그러면 모든 계정이 SSO를 거쳐야 하며, 다른 세션은 403 sso_required로 거부됩니다.

### EXTERNAL_SSO_PROVIDERS에 실수가 있으면 어떻게 되나요?

설치가 부팅을 거부합니다. 형식이 잘못된 값 때문에 공급자가 조용히 빠지거나 로그인이 절반만 구성되는 일은 없습니다.
