forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
121
docs/custom-field-jsonb-index-policy.md
Normal file
121
docs/custom-field-jsonb-index-policy.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Custom Field JSONB 및 인덱스 정책
|
||||
|
||||
## 현재 구조
|
||||
|
||||
- Tenant custom schema는 `tenants.config.userSchema` JSONB에 저장한다.
|
||||
- Tenant custom value는 backend DB의 `users.metadata` JSONB에 저장한다.
|
||||
- `isLoginId=true`인 Tenant field 값은 로그인 식별자 처리를 위해 `user_login_ids`에도 동기화한다.
|
||||
- Ory Kratos traits에는 인증/식별에 필요한 최소 값만 동기화하는 방향으로 정리한다.
|
||||
- RP custom value는 backend DB의 `rp_user_metadata.metadata` JSONB에 별도 저장한다.
|
||||
|
||||
## Tenant Custom Field
|
||||
|
||||
Tenant schema field는 다음 속성을 기준으로 한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "employeeNo",
|
||||
"label": "사번",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"indexed": true,
|
||||
"isLoginId": true,
|
||||
"adminOnly": false,
|
||||
"validation": "^[A-Z0-9]+$"
|
||||
}
|
||||
```
|
||||
|
||||
- `indexed=true`는 검색/필터 최적화 대상이라는 의미다.
|
||||
- `isLoginId=true`이면 backend와 adminfront 모두 `indexed=true`를 강제한다.
|
||||
- `isLoginId=true`는 값 필수를 의미하지 않는다. 값 필수 여부는 `required=true`로 별도 제어한다.
|
||||
- `isLoginId=true`인 field는 `type=text`만 허용한다.
|
||||
- JSONB 통합 정책에서는 `varchar` 크기 지정 의미를 두지 않는다.
|
||||
|
||||
## RP Custom Field
|
||||
|
||||
RP custom schema는 client metadata의 `customUserSchema`에 저장한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"customUserSchema": [
|
||||
{
|
||||
"key": "approvalLevel",
|
||||
"label": "승인 등급",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"indexed": true,
|
||||
"claimEnabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
RP custom value는 `rp_user_metadata` 테이블에 저장한다.
|
||||
|
||||
```text
|
||||
client_id text
|
||||
user_id uuid
|
||||
metadata jsonb
|
||||
created_at timestamptz
|
||||
updated_at timestamptz
|
||||
primary key (client_id, user_id)
|
||||
foreign key (user_id) references users(id)
|
||||
```
|
||||
|
||||
Backend API 초안:
|
||||
|
||||
```text
|
||||
GET /api/v1/dev/clients/:id/users/:userId/metadata
|
||||
PUT /api/v1/dev/clients/:id/users/:userId/metadata
|
||||
```
|
||||
|
||||
PUT payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"approvalLevel": "A",
|
||||
"preferences": {
|
||||
"theme": "dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 검색 및 인덱스
|
||||
|
||||
- `indexed=true` field만 검색 UI/API 후보로 노출한다.
|
||||
- 기본 검색은 exact match, exists, JSON containment 중심으로 제한한다.
|
||||
- RP custom field의 LIKE/fuzzy 검색은 기본 제공하지 않는다.
|
||||
- GIN 인덱스는 backend index manager가 별도 상태로 관리하는 방향을 원칙으로 한다.
|
||||
- API 요청 처리 중 `CREATE INDEX`를 동기 실행하지 않는다.
|
||||
|
||||
## Claim Projection
|
||||
|
||||
JWT 또는 userinfo 응답에서는 custom field를 top-level에 풀지 않는다.
|
||||
Tenant/RP 단위로 묶어서 전달한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"tenant_profiles": [
|
||||
{
|
||||
"tenant_id": "tenant-uuid",
|
||||
"tenant_slug": "hanmac-family",
|
||||
"fields": {
|
||||
"employeeNo": "E1001"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rp_profiles": [
|
||||
{
|
||||
"client_id": "sample-rp",
|
||||
"fields": {
|
||||
"approvalLevel": "A"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `claimEnabled=true` field만 RP claim 후보로 포함한다.
|
||||
- 긴 JSON 값은 기본적으로 token claim보다 userinfo/profile API 응답에 싣는 방향을 우선한다.
|
||||
127
docs/rp-auto-login-guide.md
Normal file
127
docs/rp-auto-login-guide.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# RP 자동 로그인 지원 가이드
|
||||
|
||||
이 문서는 Baron SSO의 userfront 연동 앱에서 RP를 클릭했을 때 별도 로그인 버튼 클릭 없이 OIDC 인증을 시작하려는 RP 등록자와 RP 개발자를 위한 기준입니다.
|
||||
|
||||
## 목적
|
||||
|
||||
자동 로그인은 userfront가 RP의 자체 로그인 시작 URL로 사용자를 보내고, RP가 그 진입점에서 OIDC Authorization Code + PKCE 흐름을 직접 시작하는 방식입니다.
|
||||
|
||||
Baron backend가 `/oauth2/auth?...` URL을 대신 만들어 넘기지 않는 이유는 RP가 직접 `state`, `nonce`, PKCE verifier/challenge, callback 검증 상태를 관리해야 하기 때문입니다. SPA나 모바일 웹앱은 이 상태가 RP 저장소에 있어야 callback을 안전하게 처리할 수 있습니다.
|
||||
|
||||
## 등록 메타데이터
|
||||
|
||||
RP는 Hydra client metadata에 다음 값을 저장합니다.
|
||||
|
||||
| 키 | 타입 | 설명 |
|
||||
| --- | --- | --- |
|
||||
| `auto_login_supported` | boolean | 자동 로그인 지원 여부입니다. `true`일 때만 userfront가 자동 로그인 URL을 진입 URL로 사용합니다. |
|
||||
| `auto_login_url` | string | RP가 OIDC 로그인을 시작하는 URL입니다. `http` 또는 `https` URL이어야 합니다. |
|
||||
|
||||
devfront의 RP 일반 설정에서 다음 항목을 입력합니다.
|
||||
|
||||
1. `자동 로그인 지원`을 켭니다.
|
||||
2. `자동 로그인 시작 URL`에 RP 로그인 시작 URL을 입력합니다.
|
||||
3. Redirect URI 목록에는 RP callback URL을 등록합니다.
|
||||
4. 저장 후 userfront 연동 앱 카드에서 “연동앱 클릭 시 별도 로그인 없이 로그인할 수 있습니다.” 안내가 보이는지 확인합니다.
|
||||
|
||||
예시:
|
||||
|
||||
```text
|
||||
auto_login_supported: true
|
||||
auto_login_url: https://org.example.com/login?auto=1
|
||||
redirect_uri: https://org.example.com/auth/callback
|
||||
```
|
||||
|
||||
## RP 구현 요구사항
|
||||
|
||||
RP는 `auto_login_url`에서 다음 동작을 구현해야 합니다.
|
||||
|
||||
1. `auto=1` 쿼리를 읽습니다.
|
||||
2. 이미 RP 세션이 있으면 기본 화면 또는 `returnTo` 경로로 이동합니다.
|
||||
3. RP 세션이 없고 `auto=1`이면 로그인 버튼을 기다리지 않고 OIDC authorization 요청을 시작합니다.
|
||||
4. OIDC 요청 전에 `state`, `nonce`, PKCE `code_verifier`, `code_challenge`를 생성합니다.
|
||||
5. `state`, `nonce`, `code_verifier`, `returnTo`는 RP origin의 안전한 저장소에 보관합니다.
|
||||
6. callback에서 `state`를 검증하고, token 교환 시 `code_verifier`를 사용합니다.
|
||||
7. 인증 완료 후 저장된 `returnTo`가 있으면 해당 경로로 이동합니다.
|
||||
|
||||
권장 URL 형식:
|
||||
|
||||
```text
|
||||
https://rp.example.com/login?auto=1
|
||||
https://rp.example.com/login?auto=1&returnTo=%2Fdashboard
|
||||
```
|
||||
|
||||
## Baron 내장 RP 기준
|
||||
|
||||
Baron 계열 RP는 다음 fallback을 사용합니다. env URL이 설정되어 있을 때만 자동 로그인 지원으로 간주합니다.
|
||||
|
||||
| Client ID | Env | 자동 로그인 URL |
|
||||
| --- | --- | --- |
|
||||
| `adminfront` | `ADMINFRONT_URL` | `${ADMINFRONT_URL}/login?auto=1` |
|
||||
| `devfront` | `DEVFRONT_URL` | `${DEVFRONT_URL}/login?auto=1&returnTo=%2Fclients` |
|
||||
| `orgfront` | `ORGFRONT_URL` | `${ORGFRONT_URL}/login?auto=1` |
|
||||
|
||||
orgfront는 `/login?auto=1` 진입 시 OIDC authorize 요청을 즉시 시작하며, 기본 callback 이후 이동 경로는 `/chart`입니다.
|
||||
|
||||
## userfront 동작
|
||||
|
||||
userfront는 backend의 linked RP 응답을 기준으로 진입 URL을 선택합니다.
|
||||
|
||||
1. `status`가 active가 아니면 진입 URL을 만들지 않습니다.
|
||||
2. `auto_login_supported=true`이면 `init_url`을 우선 사용합니다.
|
||||
3. `init_url`이 없으면 `auto_login_url`을 사용합니다.
|
||||
4. 자동 로그인 미지원이면 `init_url`이 있어도 기존 `url`로 이동합니다.
|
||||
|
||||
이 기준 때문에 `auto_login_supported=false`인 RP는 accidental auto-login을 수행하지 않습니다.
|
||||
|
||||
## 검증 체크리스트
|
||||
|
||||
RP 등록자는 다음을 확인해야 합니다.
|
||||
|
||||
1. devfront에서 `자동 로그인 지원`이 켜져 있습니다.
|
||||
2. `자동 로그인 시작 URL`이 실제 RP 로그인 진입점입니다.
|
||||
3. `auto_login_url`에 직접 접속하면 로그인 버튼 클릭 없이 Baron OIDC authorize 요청이 시작됩니다.
|
||||
4. callback URL이 Redirect URI에 등록되어 있습니다.
|
||||
5. callback 이후 RP가 `state`, `nonce`, PKCE 검증을 통과합니다.
|
||||
6. userfront 연동 앱 카드에 자동 로그인 안내 문구가 표시됩니다.
|
||||
7. userfront에서 RP 카드를 클릭하면 RP 로그인 화면에 머물지 않고 OIDC 흐름으로 진입합니다.
|
||||
|
||||
orgfront 기준 검증 명령:
|
||||
|
||||
```bash
|
||||
npm run test -- tests/orgfront-auto-login.spec.ts --project=chromium
|
||||
```
|
||||
|
||||
## 실패 시 확인할 항목
|
||||
|
||||
| 증상 | 확인 항목 |
|
||||
| --- | --- |
|
||||
| userfront에서 일반 URL로만 이동함 | RP metadata의 `auto_login_supported`가 `true`인지 확인합니다. |
|
||||
| userfront 카드에 안내 문구가 없음 | backend `/api/v1/user/rp/linked` 응답에 `auto_login_supported=true`가 내려오는지 확인합니다. |
|
||||
| RP 로그인 화면에 머무름 | RP가 `auto=1` 쿼리를 읽어 자동으로 `signinRedirect` 또는 동일한 OIDC 시작 함수를 호출하는지 확인합니다. |
|
||||
| callback에서 state 오류 발생 | userfront나 backend가 만든 `/oauth2/auth?...` URL을 직접 쓰지 말고 RP 자체 로그인 시작 URL에서 OIDC 요청을 생성해야 합니다. |
|
||||
| 등록 저장이 실패함 | `auto_login_supported=true`일 때 `auto_login_url`이 비어 있거나 `http/https` URL이 아닌지 확인합니다. |
|
||||
|
||||
## 구현 예시
|
||||
|
||||
React RP 예시입니다. 실제 프로젝트에서는 사용하는 OIDC client 라이브러리의 API에 맞춰 적용합니다.
|
||||
|
||||
```tsx
|
||||
const returnTo = searchParams.get("returnTo") || "/";
|
||||
const shouldAutoLogin = searchParams.get("auto") === "1";
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.isAuthenticated) {
|
||||
navigate(returnTo, { replace: true });
|
||||
return;
|
||||
}
|
||||
if (!shouldAutoLogin || auth.isLoading || auth.activeNavigator) {
|
||||
return;
|
||||
}
|
||||
void auth.signinRedirect({
|
||||
state: { returnTo },
|
||||
});
|
||||
}, [auth, navigate, returnTo, shouldAutoLogin]);
|
||||
```
|
||||
|
||||
중요한 점은 `signinRedirect`가 RP에서 실행되어야 한다는 것입니다. 그래야 RP가 callback 검증에 필요한 상태를 보유할 수 있습니다.
|
||||
Reference in New Issue
Block a user