Security Best Practices
logi를 올바르게 쓰기 위한 핵심 7가지.
1. PKCE는 항상 S256
code_challenge_method=S256plain은 logi가 거부합니다. 모바일/SPA/백엔드 모두 동일하게 S256 생성. (PKCE 상세)
2. redirect_uri 완전 일치
등록된 URI와 한 글자도 틀리지 않게 일치해야 합니다. 아래 모두 다른 URI로 간주됩니다:
https://app.example.com/cb
https://app.example.com/cb/ ← trailing slash
https://APP.example.com/cb ← 대소문자
https://app.example.com/cb?foo=1 ← query3. state 파라미터는 필수
/oauth/authorize 요청에 예측 불가능한 난수를 state로 포함하고, 콜백에서 세션에 저장된 값과 일치 확인. 불일치 시 CSRF로 판단하고 요청을 폐기.
const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
sessionStorage.setItem("oauth_state", state);
// 콜백에서
if (params.get("state") !== sessionStorage.getItem("oauth_state")) {
throw new Error("CSRF: state mismatch");
}4. Refresh Token은 안전한 저장소에만 저장
- ❌ 브라우저 localStorage/IndexedDB — XSS 한 번에 털림
- ✅ httpOnly Secure SameSite=Strict 쿠키 — 클라이언트 JS 접근 불가 (웹)
- ✅ 모바일 앱은 OS 보안 저장소 사용 — 플랫폼별 권장 방식이 다릅니다:
| 플랫폼 | 권장 저장 방식 | 핵심 옵션 |
|---|---|---|
| iOS Native | Keychain Services | kSecAttrAccessibleWhenUnlockedThisDeviceOnly (iCloud sync 차단) |
| Android Native | DataStore + Tink 또는 Ackee Guardian | setUserAuthenticationRequired(true) |
| Flutter | flutter_secure_storage 10.0+ | KeychainAccessibility.first_unlock_this_device |
| React Native | react-native-keychain 10.0+ | ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY |
⚠️ Android EncryptedSharedPreferences 비권장
androidx.security:security-crypto 1.1.0부터 deprecated. 신규 SDK 코드에서는 사용하지 말고 DataStore + Tink 또는 Ackee Guardian으로 마이그레이션하세요.
🔐 iCloud sync 주의
iOS Keychain은 기본값으로 iCloud Keychain에 sync됩니다. 토큰은 디바이스 바인딩이 원칙이므로 반드시 ...ThisDeviceOnly accessibility를 명시하세요. 자세한 예시는 iOS 통합 가이드를 참고하세요.
5. JWT 검증은 서명 + iss + aud + exp
await jwtVerify(token, jwks, {
issuer: "logi",
audience: process.env.LOGI_CLIENT_ID, // 내 client_id와 정확히 일치
// exp, nbf 는 jose가 자동 검증
});aud 검증을 빼먹으면 다른 logi 앱의 토큰을 오인할 수 있습니다.
6. Client Secret 관리
- 발급 시 1회 노출 — 즉시 secret manager / env에 복사
- 소스 코드/Git에 절대 커밋 금지
- 유출 의심 시
/developer/applications/:id/rotate_secret즉시 호출 — 구 secret 즉시 무효화 - CI 로그 마스킹 확인
7. HTTPS + HSTS
- 모든 OAuth 엔드포인트는 HTTPS 필수
- logi 서버는
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload기본 발송 - 제휴사 redirect_uri도 HTTPS만 등록 (localhost는 개발용 예외)
공격 시나리오 별 방어 요약
| 공격 | logi 방어 | 제휴사 측 방어 |
|---|---|---|
| code 탈취 (네트워크/로그) | PKCE S256 | verifier를 sessionStorage에만 |
| 다른 제휴사 code 주입 | code_challenge + client_secret 바인딩 | aud 검증 |
| CSRF authorize | state 검증 | state를 세션에 저장 |
| refresh token 재사용 | rotation + chain revoke | 쿠키 저장 + Set-Cookie 덮어쓰기 |
| replay authorize | nonce (openid) | nonce 검증 |
| open redirect | redirect_uri 화이트리스트 (exact) | redirect_uri 등록 엄격 |
| 브루트포스 로그인 | 10회/15분 → 30분 lockout + Cloudflare | — |
| 의심 로그인 | 국가 변경/새 device/burst 탐지 → suspicious=true | user.locked_until 체크 |
Passkey 도입 권장
비밀번호 + OTP 대신 Passkey가 가장 강한 기본값입니다:
- 피싱 불가 (origin 바인딩)
- 재사용 불가 (기기별 고유)
- 사용자 경험 최고 (Face ID 한 번)
logi iOS 앱은 가입 직후 Passkey 등록을 권장합니다. 웹 클라이언트도 navigator.credentials.create/get로 통합 가능 — OAuth Flow 이후 단계로 도입하세요.
토큰 유출 자동 무력화
토큰은 “절대 새지 않는다”가 아니라 “새도 피해 범위를 빠르게 줄인다”는 기준으로 설계해야 합니다. logi는 access token, refresh token, client secret, PAK를 서로 다른 방식으로 다루고, 유출 징후가 보이면 다음 요청부터 실패하도록 만듭니다.
Refresh token 재사용 감지
Refresh token은 사용할 때마다 새 토큰으로 교체됩니다. 정상 클라이언트라면 이전 refresh token을 다시 보낼 일이 없습니다.
- 정상 앱이 refresh token A를 사용합니다.
- logi가 A를 revoke하고 refresh token B를 발급합니다.
- 공격자가 훔쳐 둔 A를 나중에 다시 사용합니다.
- logi는 이를 재사용 공격으로 보고 같은 체인의 refresh token을 모두 revoke합니다.
이후 앱은 새 access token을 받을 수 없고, 사용자는 다시 로그인해야 합니다. 사용자가 직접 “유출 신고”를 하지 않아도 서버가 재사용을 근거로 차단합니다.
Access token revoke 확인
JWT access token은 서명만 보면 만료 전까지 유효해 보일 수 있습니다. logi는 jti를 함께 발급하고, 민감 API에서는 revoke 상태를 조회해 이미 폐기된 토큰을 거부합니다.
await jwtVerify(token, jwks, {
issuer: "logi",
audience: process.env.LOGI_CLIENT_ID
});
// 민감 요청은 jti revoke 상태까지 확인
await assertNotRevoked(payload.jti);제휴사 앱은 exp, iss, aud 검증을 반드시 수행해야 합니다. logi API 또는 introspection을 사용하는 경우 revoke된 토큰은 더 이상 active로 취급되지 않습니다.
Client secret과 PAK 유출 대응
Client secret은 앱 단위 비밀값이고, PAK는 사용자 또는 운영자가 CLI/API 자동화에 쓰는 개인 키입니다. 둘 다 유출 의심 즉시 회전 또는 revoke해야 합니다.
| 유출 대상 | 자동/즉시 대응 | 운영자가 할 일 |
|---|---|---|
| Refresh token | 재사용 감지 시 토큰 체인 revoke | 사용자 재로그인 안내 |
| Access token | revoke 상태 확인 시 요청 거부 | 짧은 만료 시간 유지 |
| Client secret | rotate 시 기존 secret 즉시 무효 | CI/CD secret 교체 |
| PAK | revoke 시 다음 API 요청부터 401 | 새 PAK 발급 후 자동화 환경 갱신 |
사용자가 보게 되는 결과
- 기존 세션 또는 연동 앱이 갑자기 로그아웃될 수 있습니다.
- 새 권한이 필요한 경우 consent 화면이 다시 표시됩니다.
- 의심 로그인 정책이 켜져 있으면 계정이 임시 잠금될 수 있습니다.
- 앱 관리자는 audit log에서 secret 회전, PAK 발급/폐기, 앱 삭제 기록을 확인할 수 있습니다.
2단계 인증과 백업 코드
2단계 인증은 비밀번호가 맞아도 추가 코드를 요구하는 보호 장치입니다. logi는 시간 기반 일회용 비밀번호(TOTP)를 사용하므로 Google Authenticator, 1Password, Authy, iCloud Passwords 같은 앱과 호환됩니다.
설정 흐름
- 사용자가 보안 설정에서 2단계 인증 켜기를 누릅니다.
- logi가 계정별 TOTP secret과 QR 코드를 생성합니다.
- 사용자는 인증 앱으로 QR 코드를 스캔합니다.
- 앱에 표시된 6자리 코드를 입력해 secret 소유를 증명합니다.
- 검증에 성공하면 2단계 인증이 활성화되고 백업 코드가 발급됩니다.
TOTP 코드는 보통 30초마다 바뀝니다. 서버와 휴대폰 시간이 크게 어긋나면 실패할 수 있으므로, 기기 시간 자동 설정을 켜두는 것이 좋습니다.
로그인 때 동작
2단계 인증이 켜진 계정은 비밀번호 검증 후 바로 로그인 완료 처리되지 않습니다. 다음 중 하나가 추가로 필요합니다.
- 인증 앱의 6자리 TOTP 코드
- 아직 사용하지 않은 백업 코드 1개
- Passkey처럼 사용자 확인(User Verification)이 포함된 강인증
성공하면 해당 세션에 otp_verified_at이 기록됩니다. 민감 작업은 이 시간이 너무 오래되었거나 없으면 다시 2단계 인증을 요구할 수 있습니다.
백업 코드
백업 코드는 휴대폰 분실, 인증 앱 삭제, 새 기기 이전 실패 같은 상황에서 계정에 들어가기 위한 비상 수단입니다.
- 발급 직후 한 번만 전체 목록을 보여줍니다.
- 각 코드는 1회만 사용할 수 있고, 사용 즉시 폐기됩니다.
- 비밀번호 관리자나 인쇄물처럼 인증 앱과 다른 장소에 보관해야 합니다.
- 남은 코드가 적어지면 새 백업 코드를 재발급하고 기존 코드는 모두 무효화하는 것이 안전합니다.
백업 코드는 편의 기능이 아니라 계정 복구 수단입니다. 평소 로그인에는 인증 앱이나 Passkey를 쓰고, 백업 코드는 기기를 잃어버렸을 때만 사용하는 기준이 안전합니다.
로그/알림 정책
logi는 다음을 자동 수행합니다:
- 모든 로그인 이벤트
login_logs에 기록 (IP, UA, country) login_notification_enabledON인 사용자에게 푸시/이메일 (Phase 2)suspicious=true로그인은 알림 OFF여도 강제 알림 (Phase 2)auto_lock_on_suspicious_loginON인 사용자는 의심 탐지 시 24시간 자동 잠금
이것들은 제휴사 측 구현이 아닌, logi의 책임입니다. 제휴사는 user.unlinked 등 Webhook 이벤트를 구독하면 자사 DB 동기화 가능.
점검 체크리스트
- [ ] 모든 엔드포인트 HTTPS?
- [ ] PKCE S256 생성·전달·저장 경로 점검?
- [ ] state 생성·검증 경로 점검?
- [ ] client_secret은 env/secret manager에만?
- [ ] Refresh Token은 서버 세션 또는 OS 키체인에만?
- [ ] JWT 검증에 iss + aud + exp 포함?
- [ ] redirect_uri 등록값과 코드의 URI 완전 일치?
- [ ] CI/로그에 secret/token 마스킹?
- [ ]
token.revokedWebhook 구독?