# 앱 사용자 환경

앱은 여러 곳에서 실행됩니다. 개발자의 노트북, CI, 스테이징 서버, 그리고 프로덕션입니다. 앱 사용자 환경은 이런 곳마다 앱 사용자 디렉터리를 따로 둡니다. 로컬 빌드에 로그인한 사람은 같은 사람이라도 프로덕션과는 다른 사용자이고, 개발용으로 만든 키로는 누구도 프로덕션에 로그인시킬 수 없습니다.

환경마다 다음을 따로 가집니다.

- **사용자**와 그 세션, 연결된 소셜 계정. 이메일 주소는 환경 안에서만 고유하므로 같은 주소가 두 환경에 서로 다른 사용자로 있을 수 있습니다.
- **키**. `identity:auth`가 있는 키는 환경 하나에 속합니다.
- **웹 출처와 리디렉션 URI**. 브라우저가 Mocco를 호출할 수 있는 곳과 소셜 로그인이 돌아올 수 있는 곳입니다.
- **소셜 제공자 앱**. Google, Apple 등의 클라이언트 ID와 시크릿입니다.
- **로그인할 수 있는 사람**. 누가 로그인할 수 있는지 정하는 규칙입니다.
- **토큰 대상(aud)**. 이 환경이 발급하는 액세스 토큰의 `aud` 클레임입니다.

발급자(issuer), 서명 키, JWKS는 프로젝트의 모든 환경이 함께 쓰고, 제공자 콜백 URL도 마찬가지입니다.

## 기본 환경 두 개

프로젝트에는 환경이 처음 필요해질 때 환경 두 개가 생깁니다. 예를 들어 **앱 사용자**(App users) 페이지를 열거나 `identity:auth` 키를 만들 때입니다.

| 환경 | 로그인할 수 있는 사람 | 토큰 대상 | 웹 출처와 리디렉션 URI | 용도 |
| --- | --- | --- | --- | --- |
| `production` | 누구나 | 프로젝트 ID | 만들어질 때 프로젝트의 앱에서 복사 | 실제 사용자 |
| `development` | 워크스페이스 멤버 | 환경 자신의 ID | 없음 | 로컬 개발과 팀 내부 작업 |

`production`은 프로젝트의 첫 번째 환경입니다. 환경을 지정하지 않은 키, 콘솔 작업, 에이전트 도구는 이 환경을 씁니다. 토큰 대상이 프로젝트 ID이므로, 환경이 생기기 전부터 프로젝트 ID를 기대하던 검증 코드는 그대로 동작합니다.

환경이 생기기 전부터 로그인을 쓰던 프로젝트에는 기존 사용자가 있는 `production`만 있습니다. 아래 방법으로 `development` 환경을 직접 추가하세요.

## 환경 추가하기

사용자를 따로 두어야 하는 실행 환경마다 환경을 하나씩 추가합니다. 예를 들면 `staging`이나 `ci`입니다. 프로젝트에는 환경을 최대 10개까지 만들 수 있습니다.

콘솔에서는 프로젝트의 **앱 사용자** 페이지를 열고 환경 선택기 옆의 **환경 추가**(Add environment)를 선택합니다. 키(영문 소문자, 숫자, 하이픈으로 32자까지)와 이름을 정합니다. 키는 나중에 바꿀 수 없습니다. 새 환경은 자기 토큰 대상을 가지며 워크스페이스 멤버만 로그인할 수 있습니다. 웹 출처와 리디렉션 URI는 그 환경의 **환경 설정**(Environment settings) 탭에서 정합니다.

에이전트는 MCP로 같은 일을 할 수 있습니다.

- `mocco_identity_environments_search`는 프로젝트의 환경을 ID, 토큰 대상, 설정과 함께 보여 주고, 제공자 콜백 URL도 알려 줍니다.
- `mocco_identity_environments_create`는 `key`, `name`과 선택 사항인 `webOrigins`, `redirectUris`로 환경을 추가합니다.
- `mocco_identity_environments_update`는 이름, 출처, 리디렉션 URI, 로그인할 수 있는 사람, 허용한 이메일, 테스트 주소를 바꿉니다.
- `mocco_identity_environments_remove`는 환경을 삭제합니다.

변경하려면 `identity:write` 스코프, 워크스페이스의 에이전트 허용, 그리고 확인 절차가 필요합니다. 환경은 워크스페이스 멤버 누구나 볼 수 있고, 소유자와 관리자만 바꿀 수 있습니다.

**한눈에 보기**(Overview) 탭은 모든 환경의 설정을 나란히 보여 주고 서로 다른 설정을 표시합니다. 스테이징이 프로덕션과 같아야 할 부분이 같은지 확인할 때 씁니다.

환경을 삭제하면 그 환경의 사용자, 세션과 연결된 계정, 제공자 앱이 함께 삭제되며 되돌릴 수 없습니다. 먼저 그 환경에 묶인 키를 폐기하세요. 프로젝트의 유일한 환경은 삭제할 수 없습니다.

## 환경의 키 받기

키의 환경은 키를 만들 때 정하며 나중에 바뀌지 않습니다. 앱을 다른 환경으로 옮기려면 새 키를 만드세요.

- **콘솔**: **API 키**(API keys) 페이지에서 `identity:auth`에 체크하면 **앱 사용자 환경**(Identity environment) 선택 상자가 나옵니다. 키로 로그인할 환경을 고릅니다.
- **MCP**: `mocco_api_keys_create`를 호출할 때 `identityEnvironmentId`에 `mocco_identity_environments_search`에서 얻은 ID를 넣습니다. 지정하지 않으면 프로젝트의 첫 번째 환경에 속합니다.

웹 앱이나 React Native 앱에는 퍼블리셔블 키(`mk_pub_…`)를 씁니다. 키 종류와 각 키를 쓰는 곳은 [API 키](https://www.mocco.club/docs/ko/articles/avn27u-api-keys)를 보세요.

## 웹 출처와 리디렉션 URI

퍼블리셔블 키는 브라우저에서 프로젝트의 웹 출처나 키가 속한 환경의 웹 출처에서만 동작합니다. 소셜 로그인은 환경의 리디렉션 URI와 정확히 같은 주소나, 환경의 웹 출처에 있는 URL로만 돌아올 수 있습니다. 자세한 규칙은 [로그인이 돌아올 곳 허용하기](https://www.mocco.club/docs/ko/articles/yhw2j7-social-sign-in#2-allow-where-sign-in-may-return)에 있습니다.

주소는 그 주소를 쓰는 환경에 넣습니다.

- `production`: `https://app.example.com`, `myapp://auth/callback`
- `staging`: `https://staging.example.com`
- `development`: `http://localhost:5173`, `myapp-dev://auth/callback`

`localhost`와 다른 루프백 주소는 `development`(또는 실제 사용자가 들어오지 않는 다른 환경)에만 넣으세요. `production`은 프로젝트의 앱 설정에서 복사되었으므로, 예전에 넣어 둔 로컬 주소가 남아 있지 않은지 확인하세요.

## 소셜 제공자

제공자 앱은 환경마다 따로 둡니다. 환경 선택기에서 원하는 환경을 고른 뒤 **로그인 방식**(Sign-in methods) 탭에서 설정하거나, `mocco_identity_providers_set`에 `environmentId`를 넣어 설정합니다.

제공자마다 환경별로 OAuth 앱을 따로 만드는 것을 권장합니다. 예를 들면 "Example (development)"와 "Example (production)"입니다. 이렇게 하면 개발용 앱은 제공자의 테스트 모드에 둘 수 있고, 개발용 시크릿이 새어도 프로덕션에는 영향이 없습니다.

모든 환경이 같은 콜백 URL을 쓰므로, 각 제공자 앱에 같은 주소를 등록합니다.

```text
<IDENTITY_ISSUER>/api/ext/v1/identity/oauth/<provider>/callback
```

**환경 설정** 탭에 제공자별 콜백 URL이 나옵니다. Mocco는 콜백이 어느 환경의 것인지 시작한 로그인에서 알아내므로, 콜백 URL을 함께 써도 환경이 섞이지 않습니다.

소셜 계정으로 로그인하던 사용자를 가져온다면, 사용자를 넣을 환경에 지금 시스템이 쓰는 제공자 앱을 설정하세요. 제공자는 같은 사람에게도 앱마다 다른 ID를 줍니다. [기존 사용자 옮겨 오기](https://www.mocco.club/docs/ko/articles/yhw2j7-social-sign-in#7-move-existing-users-in)를 보세요.

## 로그인할 수 있는 사람

환경마다 누가 로그인할 수 있는지 정합니다. **환경 설정** 탭의 **로그인할 수 있는 사람**(Who can sign in)에서 고르거나, `mocco_identity_environments_update`의 `signInAccess`로 정합니다.

| 설정 | 값 | 로그인할 수 있는 사람 |
| --- | --- | --- |
| 누구나 | `open` | 이메일 주소나 소셜 계정이 있는 누구나 |
| 워크스페이스 멤버 | `members` | 워크스페이스에 참여한 멤버의 Mocco 계정 이메일과 같은 주소 |
| 등록한 주소 | `allowlist` | **허용한 이메일**(Allowed emails)에 있는 주소, 또는 등록한 `@domain`의 모든 주소(하위 도메인 제외) |

주소는 대소문자를 구분하지 않고 비교합니다. 소셜 로그인은 제공자가 인증한 이메일로 규칙을 확인합니다. `members`나 `allowlist`에서는 인증된 이메일이 없는 소셜 계정을 거부합니다.

규칙은 로그인할 때 확인합니다. 규칙을 좁혀도 이미 로그인한 사람의 세션은 계속 갱신되므로, 바로 로그아웃시키려면 **사용자**(Users) 탭에서 그 사람의 세션을 폐기하세요.

QA 팀이나 고객사가 들어와야 하는 스테이징 환경은 보통 `allowlist`에 `@example.com`을 등록하는 방식이 잘 맞습니다.

## 엔드투엔드 테스트용 테스트 주소

엔드투엔드 테스트는 메일함을 읽을 수 없습니다. **테스트 주소**(Test addresses)를 켜면, 로컬 부분이 `+mocco_test`로 끝나는 주소는 이메일을 받지 않고 고정 코드 `424242`로 로그인합니다. 로그인 규칙도 건너뛰므로 워크스페이스 멤버만 로그인할 수 있는 환경에서도 동작합니다.

**테스트 주소는 누구나 로그인할 수 없는 환경에서만 동작합니다.** 코드가 공개되어 있으므로 Mocco는 로그인할 수 있는 사람이 **누구나**인 동안 테스트 주소를 켜지 않고, 테스트 주소가 켜진 동안 로그인을 누구나에게 열지 않습니다. `production`에서는 테스트 주소를 쓰지 마세요.

환경의 **환경 설정** 탭에서 켜거나, `mocco_identity_environments_update`에 `testAddresses: true`를 넣습니다. 그다음 테스트에서 사람이 로그인하는 것과 같은 방법으로 로그인합니다.

```ts
import { createIdentityClient } from '@mocco/js/identity';

// `storage` keeps the session, as in the social sign-in guide; an in-memory Map is enough in a test.
const identity = createIdentityClient({ publishableKey: process.env.MOCCO_KEY!, storage });

const { challengeId } = await identity.emailStart('someone+mocco_test@example.com');
const session = await identity.emailVerify(challengeId, '424242');
```

HTTP로 직접 호출할 수도 있습니다.

```sh
curl -s https://api.mocco.club/v1/identity/email/start \
  -H "X-Mocco-Key: $MOCCO_KEY" -H 'Content-Type: application/json' \
  -d '{"email":"someone+mocco_test@example.com"}'
# → {"challengeId":"…"}

curl -s https://api.mocco.club/v1/identity/email/verify \
  -H "X-Mocco-Key: $MOCCO_KEY" -H 'Content-Type: application/json' \
  -d '{"challengeId":"…","code":"424242"}'
```

브라우저 테스트에서는 앱의 로그인 화면에 테스트 주소와 `424242`를 입력하면 됩니다.

평소 제한은 그대로 적용됩니다. 코드는 10분 뒤 만료되고, 틀린 코드도 5번의 시도 횟수에 들어가며, 로그인 시작 요청에는 속도 제한이 있습니다. 테스트를 병렬로 돌린다면 실행마다 다른 테스트 주소(예: `run-1042+mocco_test@example.com`)를 쓰세요. 테스트 주소는 이메일 코드에만 적용되고, 소셜 로그인은 항상 로그인 규칙을 따릅니다. 설정을 끄면 `424242`로는 누구도 로그인할 수 없습니다.

## 토큰 대상(aud)

액세스 토큰의 `aud`에는 환경의 토큰 대상이 들어가고, 한 환경의 토큰은 다른 환경에서 검증에 실패합니다. 토큰은 키가 속한 환경의 토큰 대상으로 검증하세요.

- **`/config`에서 읽기.** 환경의 키로 `GET /v1/identity/config`를 호출하면 그 환경의 `audience`와 `environmentKey`가 옵니다. 백엔드는 로그인을 켜기 전에 이 값을 기대하는 값과 비교할 수 있습니다.
- **SDK에 넘기기.** Node에서는 `createIdentityVerifier({ projectId, audience })`, Rust에서는 `VerifierConfig { audience: Some(…), … }`입니다. [서버에서 앱 사용자 토큰 검증하기](https://www.mocco.club/docs/ko/articles/jhr84i-verify-tokens)를 보세요.

`production`의 토큰 대상은 프로젝트 ID이고 검증기의 기본값도 프로젝트 ID이므로, 프로덕션 백엔드는 바꿀 것이 없습니다. 다른 환경의 토큰 대상은 그 환경 자신의 ID입니다. 키와 같은 설정에 두세요. 예를 들면 `MOCCO_KEY` 옆에 `MOCCO_IDENTITY_AUDIENCE`를 둡니다.

## 로컬 개발 설정

1. **앱 사용자** 페이지에서 `development`로 바꾸고, **환경 설정**의 **웹 출처**(Web origins)에 개발 서버의 출처를 추가합니다. 예: `http://localhost:5173`. 네이티브 빌드에서 소셜 로그인을 쓴다면 `myapp-dev://auth/callback` 같은 리디렉션 URI도 추가합니다.
2. **API 키** 페이지에서 `identity:auth`가 있는 퍼블리셔블 키를 만들고 **앱 사용자 환경**에서 `development`를 고릅니다.
3. 키와 토큰 대상을 git이 무시하는 `.env` 파일에 넣습니다. 토큰 대상은 `/config`나 환경 설정에서 읽습니다.

   ```sh
   # .env (listed in .gitignore)
   MOCCO_KEY=mk_pub_…
   MOCCO_IDENTITY_AUDIENCE=…
   ```

4. 앱을 띄우고 Mocco 계정의 이메일 주소로 직접 로그인합니다. `development`는 워크스페이스 멤버를 받아 줍니다. 멤버가 아닌 팀원은 **등록한 주소**로 추가할 수 있습니다.
5. 메일함 없이 로그인하려면 **테스트 주소**를 켜고 `you+mocco_test@example.com`과 코드 `424242`를 씁니다.

프로덕션 배포에는 `production` 환경의 다른 키를 호스팅 서비스의 시크릿에 넣어 씁니다. 코드는 같고 키와 토큰 대상만 다릅니다.

## 문제 해결

| 보이는 것 | 원인 | 해결 |
| --- | --- | --- |
| `403 insufficient_scope` | 키에 `identity:auth`가 없습니다 | `identity:auth`가 있는 키를 만들고 환경을 고릅니다 |
| `403 identity_environment_required` | 키가 앱 사용자 환경에 묶여 있지 않아 로그인시킬 사용자 디렉터리가 없습니다 | `identity:auth`가 있는 새 키를 만들어 환경을 고르고, 예전 키를 폐기합니다 |
| `403 sign_in_not_allowed`, 또는 소셜 리디렉션에 `error=sign_in_not_allowed` | 환경의 로그인 규칙이 그 주소를 받아 주지 않습니다 | 멤버의 계정 이메일을 쓰거나, 주소나 도메인을 **허용한 이메일**에 추가하거나, 테스트 주소가 켜진 환경이면 테스트 주소를 씁니다. `members`나 `allowlist`에서는 소셜 계정에 인증된 이메일이 있어야 합니다 |
| `403 origin_not_allowed` | 페이지의 출처가 프로젝트의 웹 출처에도, 키가 속한 환경의 웹 출처에도 없습니다 | 키가 속한 환경에 출처를 추가합니다. 다른 환경의 키는 그 환경의 출처에서 거부됩니다 |
| `400 invalid_redirect_uri` | 소셜 리디렉션 주소가 환경의 리디렉션 URI나 웹 출처에 없습니다 | 키가 속한 환경에 추가합니다 |
| 검증기가 모든 토큰을 거부합니다(Node `authenticate`가 `null`, Rust가 `InvalidToken`) | 검증기가 다른 환경의 토큰 대상을 기대합니다. 키는 `development`인데 프로젝트 ID로 검증하는 경우가 흔합니다 | 그 키로 `/config`에서 읽은 `audience`를 넘깁니다 |
| `424242`가 거부됩니다 | 테스트 주소가 꺼져 있거나, 주소가 `+mocco_test`로 끝나지 않거나, 요청이 만료되었거나 시도 횟수를 다 썼습니다 | 환경의 **테스트 주소** 설정을 확인하고 로그인을 새로 시작합니다 |
| 테스트 주소를 켤 수 없습니다 | 로그인할 수 있는 사람이 **누구나**입니다 | 먼저 **워크스페이스 멤버**나 **등록한 주소**를 고릅니다 |
