API key와 client-secret¶
두 credential은 목적과 수명주기가 다릅니다. API key는 워크스페이스에 귀속된 장기 읽기 credential이고, client-secret은 로그인 회원이 실시간 세션을 위해 발급받는 단기 Ed25519 JWT입니다.
워크스페이스 API key¶
- 발급:
POST /v1/workspaces/api-keys - 권한:
X-Workspace-Id대상 워크스페이스의OWNER - 형식: 환경별 접두 +
SecureRandom32바이트를 base62로 인코딩한 랜덤 부분 - 저장: 원본이 아니라 SHA-256
secret_hash와 표시용key_prefix - 노출: 발급 응답에서만 원본을 보여 주며 이후 목록에는 포함하지 않음
- 사용:
Authorization: Bearer <api-key> - 권한:
SCOPE_WORKSPACE_READ - 폐기: 물리 삭제가 아니라
REVOKED상태 전환
API key 목록은 원본과 해시를 제외하고 ID, 이름, 접두, 종류, 상태, 최근 사용 시각, 생성 시각만 반환합니다. 운영 프로파일은 LIVE, 기본·개발 프로파일은 TEST 종류를 발급합니다.
client-secret 발급¶
sequenceDiagram
participant C as 로그인 클라이언트
participant A as App Backend
participant DB as PostgreSQL
participant S as Socket Backend
C->>A: POST /v1/workspaces/client-secrets<br/>Bearer access JWT + X-Workspace-Id
A->>A: HMAC access JWT 검증
A->>DB: ACTIVE 멤버십 확인
A->>A: TTL clamp(300..10800, 기본 3600)
A->>A: Ed25519 개인키로 JWT 서명
A-->>C: clientSecret.value + 실제 TTL + expiresAt
Note over A,DB: client-secret은 DB에 저장하지 않음
C->>S: WebSocket 연결에 client-secret 제출
S->>S: Ed25519 공개키로 서명·claim·만료 검증
발급 요청 본문은 생략하거나 {"expiresInSeconds": 3600}으로 보냅니다. 값이 범위 밖이면
400을 반환하지 않고 300초 또는 10,800초로 보정합니다.
client-secret의 JOSE·claim 계약은 다음과 같습니다.
- header:
alg=EdDSA,typ=JWT iss=sohri-app-backendaud=sohri-realtime단일 문자열sub=<memberId>workspaceId=<workspace UUID>jti=<random UUID>iat,exp는 epoch seconds이며exp = iat + 실제 TTLapiKeyId는 포함하지 않음
일회용 토큰이 아닙니다
응답에서만 전달되고 서버가 저장하지 않는다는 의미이지, 한 번 사용하면 폐기된다는 뜻이
아닙니다. 사용 횟수나 jti 소비 상태를 기록하지 않으므로 유효 기간 안에는 재사용할 수
있습니다. 탈취 위험을 TTL과 전송 채널 보호로 제한해야 합니다.
키 분리와 서비스 경계¶
로그인 JWT는 HMAC 대칭키, client-secret은 Ed25519 개인키로 서명합니다. Socket Backend에는 client-secret 검증용 공개키만 배포해야 하며 로그인 HMAC 비밀키를 공유할 이유가 없습니다. 발급 뒤 App Backend가 Socket Backend를 HTTP로 호출하거나 토큰을 전달하는 단계도 없습니다.
회원·워크스페이스 검증은 App Backend가 발급 시 수행하고, 실시간 연결의 토큰 검증은 Socket Backend가 자체 수행합니다. 상세 인증 흐름은 로그인과 토큰 인증, 현재 키 배선 위험은 보안과 현재 격차를 참고하세요.